4D3 Computer Graphics OpenGL Project Report

Mikhail Volkov

04400593

January 18, 2010

Department of Electronic & Electrical Engineering Trinity College Dublin Contents

I User Documentation 3

1 Introduction 3

2 Getting Started 4 2.1 Operating System Requirements ...... 4 2.2 Setup ...... 4 2.3 Controls ...... 4

II Developer Documentation 5

1 Technical Overview 5 1.1 Why #? ...... 5 1.2 Why Tao? ...... 5 1.3 Why FreeGlut? ...... 6

2 Implementation Details 6 2.1 Navigation ...... 7 2.1.1 Mouse Look ...... 7 2.1.2 WASD Movement ...... 8 2.2 Representation ...... 10 2.3 Physics and Collision Detection ...... 13 2.3.1 Collisions between Entities and Bullets ...... 13 2.3.2 Collision with Walls ...... 14 2.3.3 Bullet Management ...... 16 2.4 Models and Animation ...... 17 2.4.1 Hierarchical Spider Models ...... 17 2.4.2 Animation ...... 21 2.4.3 Animated Sky ...... 23 2.5 Gameplay and AI ...... 25 2.5.1 Spider Bite Handling ...... 25 2.5.2 Spider AI ...... 25 2.5.3 Spider Eggs ...... 26 2.6 Stereo Display Mode ...... 26

3 Sources 31

4 Final Remarks 31 Part I User Documentation

1 Introduction

Beware of the Spider was meant to be an introspective exploration of my irrational fear of spiders. Armed with a flak cannon, the protagonist navigates through a maze, slaughtering giant arachnids and leaving a trail of blood and carnage in his wake. The objective of the game is to find and destroy the (obviously evil) Gargantuan Spider Queen. But it is no easy feat! The labyrinth is infested with her progeny and the floors are littered with spider eggs. You start with 100 life and 200 shells of ammunition. If a spider sees you he will chase until he catches you, or until you are out of sight. If a spider catches you he will commence feasting on your blood and you will continuously lose life. Should your life reach zero, you will be dead. Finally, you should tread carefully around spider eggs or you may be in for a nasty surprise. You have been warned! The game features a first-person view, WASD navigation with mouse look, collision detection with the environment and between entities, animated hierarchical spiders, enemy AI, and last but not least, in the true spirit of cheesy horror movies from the sixties, the game features an old school red-cyan anaglyph mode.

Figure 1: Startled people watching a 3D movie

3 2 GETTING STARTED

2 Getting Started

2.1 Operating System Requirements The game uses the resolution of your primary monitor and runs in fullscreen. It was developed and tested on Windows Vista (32-bit), and it runs very smoothly at 1280×800 and 1280×1024 resolutions. It does not run well on Windows XP! The mouse and keyboard sensitivities do not agree between the two operating systems, and it is generally slow for some unknown reason. Avoid running it on XP, as it will ruin the game.

2.2 Setup Beware of the Spider requires that you have the latest FreeGlut binaries (available at http://freeglut.sourceforge.net) and the latest version of the Tao Frame- work (available at http://sourceforge.net/projects/taoframework) installed on your computer. Once everything is set up, you can run the .exe or open the C# solution and run using Visual Studio. You must run the .exe from its original directory as it will look for resources in specific locations.

2.3 Controls The basic controls allow you to navigate through the maze and to shoot. This is all you need to play the game. With the advanced controls you can enable stereo mode and change the stereo settings. The default stereo settings are optimized for most people on most display monitors and you are advised not to change them. However, not everybody perceives stereo in the same way, and you can fine-tune the stereo settings using the arrow keys if you really want. The focal length determines the point of the scene which is on the projection plane. In other words, everything in front of that point will be perceived as coming out of the screen and everything behind that point will be perceived as going into the screen. The interocular distance is the distance between the two cameras of the stereo view, and is meant to correspond to the distance between the eyes.

W Move forward V Toggle stereo mode S Move backward Up arrow Increase focal length A Strafe left Down arrow Decrease focal length Strage right Left Arrow Increase interocular distance Mouse Look Right arrow Decrease interocular distance Left click Shoot P Toggle wireframe mode (cheating!) Table 1: Basic controls Table 2: Advanced controls

4D3 Computer Graphics 4 Mikhail Volkov OpenGL Project Report Part II Developer Documentation

1 Technical Overview

Beware of the Spider is written in C# using the . The Tao Framework is a collection of bindings to facilitate cross-platform media application development utilizing the .NET and platforms. For this game I used the Tao.OpenGl 2.1.0.12 and Tao.FreeGlut 2.4.0.2 assemblies.

1.1 Why C#? To a large extent it comes down to personal preference. I am a big fan of .NET and I was curious to see how a computationally demanding application such as a 3D game would cope compared with a native C++ solution, for example. Furthermore, C# is a powerful, expressive and elegant language. Throughout this report I will allude to this claim, giving specific examples of things I did with C# that you cannot do with C++, and how it improved the design of my game.

1.2 Why Tao? There are several possibilities for using OpenGL with C#.

• SharpGL: I started off with SharpGL. It is very easy to use, but I found it to be somewhat buggy and unstable. SharpGL requires you to instantiate an OpenGL object, rather than making static calls from a namespace, which felt somewhat unintuitive. It also features a Scene Graph interface which makes it much easier to work with certain OpenGL structures like cameras and textures. However, it obfuscates the underlying principles and was not good from a learning point of view.

• CsGL: This was the second API I tried. CsGL felt much more robust than SharpGL, but it is not supported any more and most of its user base seems to have migrated to Tao.

• Tao: Tao has been around for a number of years and it is the most established OpenGl framework for .NET, with a large developer community. Tao features bindings for the entire OpenGL API, as well as FreeGlut and many other third party libraries.

• OpenTK: OpenTK is cross-platform, offers a number of helper libraries, and is growing in popularity. But Tao is more mature, and OpenTK is actually based on Tao so in the end I decided to use Tao and did not get a chance to try OpenTK.

5 2 IMPLEMENTATION DETAILS 1.3 Why FreeGlut?

Finally, it is worth a mention that a .NET framework can be used with any .NET language, so C++/CLI was another tempting possibility. We have the benefits of a managed environment and the ability to drop down to native C++ for performance bot- tlenecks. This would also have been possible in combination with FreeGlut. However, I believe C# offered more advantages as a language.

1.3 Why FreeGlut? Tao is simply a collection of bindings for the native OpenGL binaries. In order for OpenGL to work it requires a device and a rendering context, and we have exactly the same options to choose from under Tao. These are some of the options that I considered:

• Glut/FreeGlut: This is the course standard. It is fast, cross-platform and very easy to use. It is also very readable and a lot of examples on the internet use FreeGlut. Given that I am already deviating from the course in my choice of programming language, I was reluctant to also use a different window manager, unless I saw very a compelling reason to do so.

• SimpleOpenGlControl: This is a widget that creates an OpenGl context as a control. It means that we can select it from the toolbox in Visual Studio and drag and drop it into our Windows Forms application. In- fact we can have multiple controls in one application. This is very powerful and I used it in the initial stages of the game development as a rapid prototyping environment. But ultimately it is not suitable for this sort of game as it slow, and in most cases users would prefer to play a first-person shooter in fullscreen so they probably would not need or want buttons and menus.

• Win32: This is the context used in the NeHe tutorials. It sets up a Win32 environment and compares quite favorably with FreeGlut in terms of performance. Although it offers more control than FreeGlut at a low level, it also poses more potential problems. I saw no reason to use this, other than for the sake of it.

• SDL: Another good alternative and we get the extra benefits of sound and True- Type fonts. Tao also has a wrapper for SDL so this would have been easy to do. I have no idea about performance though, and I did not see a really convincing reason to choose this over FreeGlut.

2 Implementation Details

This section will detail all the notable features in Beware of the Spider. They are listed roughly in the order I worked on them and this reflects what I believe to be the most appropriate order in the development cycle of this kind of game. Having chosen .NET as my development environment, I was always going to be on the backfoot in terms of performance and I took the issue of optimization very seriously.

4D3 Computer Graphics 6 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.1 Navigation

As well as outlining the theoretical and implementation details, where applicable I will also provide a discussion of the steps I took to ensure that performance was optimized.

2.1 Navigation This was the starting point in the development of the game. I started off with noth- ing except the three Cartesian axes rendered onto the screen. This was enough to implement and test navigation.

2.1.1 Mouse Look Being an avid fan of first-person shooters, I was keen to implement mouse look. When Doom first changed the face of FPS games in the nineties, millions of gamers played it without using the mouse. But games have come a long way since, and without mouse look I felt the game would have been difficult to enjoy. There are several ways of approaching this. I chose to use spherical geometry (Figure 2) as it was easy to derive from first principles.

Figure 2: Spherical coordinates

FreeGlut provides two callbacks for mouse motion, glutMotionFunc and glutPas- siveMotionFunc which take the (x, y) position of the mouse cursor on the screen as the arguments. To implement mouse look, two angles are defined, the azimuth (θ) and the elevation (φ). As the player moves the mouse, these two angles are calculated as the difference between the current mouse position and the center of the screen. The elevation is bounded between 0 and π so that the player does not turn upside down as he looks past straight up or straight down1. Then, to convert these angles into a direc- tion vector we simply use the following expressions, which convert spherical coordinates (r, θ, φ) into Cartesian coordinates (x, y, z):

1This is typical of a first-person view. The “highest” point you can look at is straight up and the “lowest” point you can look at is straight down. You cannot “bend over backwards.”

4D3 Computer Graphics 7 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.1 Navigation

x = r cos θ sin φ y = r sin θ sin φ z = r cos φ Because the direction vector is a unit vector, we can simply set r equal to one. Listing 1 shows the C# code implementing this logic, which converts an (x, y) mouse position on the screen into the resulting direction vector.

1 internal void MouseLook(int x, int y) 2 { 3 Az = x - Game.WindowCenter.X; 4 El = y - Game.WindowCenter.Y; 5 6 if (El < 1) 7 El = 1; 8 if (El > 179) 9 El = 179; 10 11 float CosAz = (float)System.Math.Cos(Az * System.Math.PI / 180); 12 float SinAz = (float)System.Math.Sin(Az * System.Math.PI / 180); 13 float CosEl = (float)System.Math.Cos(El * System.Math.PI / 180); 14 float SinEl = (float)System.Math.Sin(El * System.Math.PI / 180); 15 16 Dir.x = CosAz * SinEl; 17 Dir.y = CosEl; 18 Dir.z = SinAz * SinEl; 19 Dir.Normalize(); 20 } Listing 1: Mouse look This approach is very primitive and has a lot of drawbacks. Firstly, the mouse movement is not smooth, since it is essentially a function of the (x, y) mouse position on the screen, which is discrete. A difference of 1 pixel is exacerbated when converted into a direction vector. This is not noticeable in general gameplay, but does become apparent if one attempts to aim very precisely at an object that is very far away. Another problem is that although the cursor is hidden and replaced by the crosshair, it is still moving around the screen so when it reaches the screen boundary the mouse look stops working. But this only happens if the player constantly rotates in one direction, which does not occur very often in general gameplay. Lasly, this method suffers from what I later discovered to be a well-established problem called gimbal lock. The solution to all these problems was to use quaternions for rotation, using the so-called arcball method for example, but this was beyond the scope of this project.

2.1.2 WASD Movement For movement there are two alternatives. The first is to poll for a key being down, and if it is then the position of the player is updated continuously in the update loop. The

4D3 Computer Graphics 8 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.1 Navigation

second alternative is to poll for a key press and a key release, and to update velocity rather than position. Thus if the a key is pressed, we increase the velocity and if a key is released we decrease the velocity; and the position of the player is updated according to the velocity in the update loop. I tried both, and the second one proved to be much more responsive so this is the method used in the game. FreeGlut provides two callbacks, glutKeyboardFunc and glutKeyboardUpFunc, which are called when a key is pressed or released, respectively. The Player class exposes an interface of four boolean properties, one for each of the four WASD navigation keys, which can be set or reset depending on whether the key is pressed or released. Listing 2 shows an example.

1 void KeyboardCallback(byte key, int x, int y) 2 { 3 switch (Char.ToUpper((char)key)) 4 { 5 ... 6 7 // Player movement 8 case ’W’: 9 player.KeyPressUp = true; 10 break; 11 12 ... Listing 2: Key press example

Inside the Player class, the implementation is a little more complicated. Movement should be independent of the direction the player is facing. Again there are two ways of achieving this. When a player rotates, we can rotate the world around the camera in the opposite direction so that the player is always facing forward in world co-ordinates. Movement becomes simple, and the advantage is that the camera only needs to be projected once, but this method is not very intuitive and regardless of this, I would need to cast multiple projections for the stereo mode. The second alternative is to keep the world stationary, rotating the camera when the player rotates, and to calculate the correct angles for movement based on the current view direction. The second method is used in the game, and it complements the mouse look implementation quite well. We simply disregard the elevation and work out how the azimuth corresponds to world coordinates, and then adjust the velocity using basic trigonometry. The azimuth is taken as the angle counter-clockwise angle between the direction vector rotation about the y-axis and the positive x-axis. This choice is arbitrary, but does affect the logic. The C# code implementing movement is shown in Listing 3 below.

1 float CosAz = (float)Math.Cos(Az * Math.PI / 180); 2 float SinAz = (float)Math.Sin(Az * Math.PI / 180); 3 4 Vel = new Vector3f(); 5

4D3 Computer Graphics 9 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.2 Representation

6 // W key pressed 7 if (KeyStates[Keys.Up]) 8 { 9 Vel.x += CosAz; 10 Vel.z += SinAz; 11 } 12 // S key pressed 13 if (KeyStates[Keys.Down]) 14 { 15 Vel.x -= CosAz; 16 Vel.z -= SinAz; 17 } 18 // A key pressed 19 if (KeyStates[Keys.Left]) 20 { 21 Vel.x += SinAz; 22 Vel.z -= CosAz; 23 } 24 // D key pressed 25 if (KeyStates[Keys.Right]) 26 { 27 Vel.x -= SinAz; 28 Vel.z += CosAz; 29 } 30 31 Vel.Normalize().Multiply(Speed); 32 33 Pos.Add(Vel); Listing 3: Movement

The velocity is normalized at the very end. This is required so that the velocity is not additive if a player is pressing two keys at once. After this, the velocity is multiplied by the speed. Once we have updated the position and direction vectors, we simply use gluLookAt to display the scene.

2.2 Representation The next step was to construct the world. The 3D model of the maze is constructed from a file, which features a 2D Nethack-style symbolic representation of the 3D model. This approach has several advantages over hard-coding the maze. It allowed me to develop the game without worrying about gameplay in advance. It also allowed me to conduct testing much more easily. For example when I needed to test collision detection I could easily remove everything except one wall and one spider. The representation of the labyrinth is broken down into manageable constructs. A Vertex contains an (x, y, z) vector representing its position in 3D space and a (u, v) vector mapping a texture coordinate to the vertex. A Triangle contains three such Vertex objects and a Quad contains two Triangles (which are right angled and aligned along their hypotenuses, forming a rectangle) and a normal vector to the plane that

4D3 Computer Graphics 10 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.2 Representation the rectangle lies on. Different sections of the maze are represented as collections of Triangle or Quad objects. Figure 3 shows the most basic representation of the maze in the 2D file. The @ symbol represents the player and the + symbol represents walls. From this we can see that a single wall is not flat, but has the same area as a single empty space. In the 3D model, a wall must therefore be represented by four Quad objects. In the most general sense, the task of converting this maze file into a 3D model of the maze involves finding the location of each + symbol in the text file, converting this 2D location into a 3D location in world coordinates and creating four Quads at that location. But it becomes immediately apparent that this is quite wasteful, as some walls will be adjacent horizontally and some adjacent vertically, and we will end up creating more Quads than we are able to see, with no easy way to discriminate between them. This would have made collision detection very inefficient. So the next step was to differentiate them into what I called xy-walls and yz-walls. xy- walls (- symbol) are parallel to the xy-plane and yz-walls (| symbol) are parallel to the yz-plane. In some cases we obviously need both (+ symbol). Using this representation we have already nearly halved the number of Quads required to represent the same maze. Figure 4 shows the resulting maze file.

++++++++++++++++++++++++ +------+------+ + + + | | | + + +++++ + | | +---+ | + + + + | | | | + ++++++++++ + + + | +------+ | + | + + + + + | + | | | + ++++++++++ ++++++ | +------+ +----+ ++++++++ + + +------+ | | + + + + | | | | + ++++++ +++++ ++++ ++++ | +----+ +-+-+ +--+ +--| + + + | + | + + + ++++ + | + + +--+ | ++++++++++ +++++++++++ +------+ +------+ + + + + | + | | + +++ +++ +++ + | +-+ +-+ +++ | + +++++++++++++++ + | +------+ | + ++ +++ + +++++ | ++ +-+ | +---| + + ++ + + + | + ++ | | | +++ +++ ++++++ + + + +-+ +-+ +----+ + | | + +++ + ++++ + + +++ + +--+ | + ++ ++ + + +| ++ | + @ +++++++++++++++++ + @ +------+ ++++++++ ++++++++

Figure 3: Maze file – basic representa- Figure 4: Maze file – using different tion orientations for walls

4D3 Computer Graphics 11 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.2 Representation

This is satisfactory, but again thinking ahead to collision detection, this is still a lot of walls to check against. I decided to create different “types” of walls. As well as allowing me to assign different textures to them, it means that an entity will only have to check for collisions with only those walls that are in the entity’s proximity, thus significantly improving efficiency. Figure 6 shows the resulting maze file. The symbols correspond to Figure 4. Now we are nearly there. I introduced symbols to represent towers (groups of four Triangles), to sit on top of some of the walls and make the maze look prettier and more distinctive. The floors are also formed using Quads with associated textures so I introduced different symbols for different floors. Finally, I used letters (s,S,o,O,Q) to represent the different spiders in the game (the floor under the spider is the floor corresponding to the part of the maze (level) the spider is located in). The final maze file (Figure 7) looks rather confusing but contains all the information describing the maze and its inhabitants. The logic for reading in the maze file and creating the world is contained in Cre- ateMaze and CreateModel in World.cs. Lambda expressions are used to create tempo- rary methods within methods, helping to keep the overall structure of the code more readable. Figure 5 shows an example of the finished labyrinth model, with walls and towers.

Figure 5: The walls and towers of the labyrinth

4D3 Computer Graphics 12 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.3 Physics and Collision Detection

#======#˜˜˜˜˜˜˜˜˜˜# ˆ======ˆ˜˜˜˜˜˜˜˜˜˜ˆ ;;: ;O’OO’O’O’O’;‘‘‘‘‘‘s‘‘‘: ; ; #˜˜˜# : ;’O’O’’’’QO’;‘‘‘*˜˜˜*‘‘: ;;:: ;O’O’’O’’O’’;‘‘s:oo‘‘‘‘: ; #======# : # : ;’’#======#oo‘:‘‘*s‘s: ;#::: ;O’#s‘‘‘o‘‘o‘o‘‘:‘s:‘oo: ; #˜˜˜˜˜˜˜˜# #˜˜˜˜# ;soo‘os*˜˜˜˜˜˜˜˜*‘*˜˜˜˜ˆ #˜˜˜˜˜˜# : : ˆ*˜˜˜˜˜*‘‘‘o‘‘‘‘‘‘:oooo: :::: :‘s:o‘s‘s‘o‘os‘‘s‘:‘‘s‘: : #˜˜˜˜# #˜#˜# #˜˜# #˜˜: :‘*˜˜˜˜*‘ˆ˜ˆ˜ˆ‘*˜˜*‘*˜˜: :#: :‘‘‘‘*‘‘‘‘o‘o‘‘‘‘‘‘‘‘‘‘: : # # #˜˜# : :s‘‘‘‘‘‘s*‘‘o*s‘‘*˜˜*‘s: #˜˜˜˜˜˜˜˜# #˜˜˜˜˜˜˜˜˜# ˆ˜˜˜˜˜˜˜˜ˆ‘o‘ˆ˜˜˜˜˜˜˜˜˜ˆ | + | | |.+.S.|...... S| | +-+ +-+ +++ | |...+-+.S.+-+...... +++.| | +------+ | |.+------+S..S..| | ++ +-+ | +---| |S.++....+-+....|..+---| | + ++ | | | |...... +...++S|....|S| +-+ +-+ +----+ + | | %-%,,%-%...+----+.+S.|.| / /++ + +--+ | /,,,,,,/++.+.S....+--+.| / /| ++ | /,,,,,,/|S....++S...... | / @ /------+ /,,@,,,/------% //////// ////////

Figure 6: Maze file – using different Figure 7: Maze file – final version in- types of walls cluding floors, towers and enemies

2.3 Physics and Collision Detection At the heart of pretty much all physical interaction are just three geometry func- tions contained in Geometry.cs. SphereSphereIntersect and PlaneSphereIntersect are boolean functions which simply return true or false depending on whether two spheres, or a sphere and a plane intersect, respectively. RayPlaneIntersect uses ray- tracing to check for the intersection of a ray and a plane, and by its nature it will always return true unless the ray is parallel with the plane, but it is useful because it also returns the point of intersection and the parameter to obtain this point. The maths behind these three methods is well documented online, in the lecture notes, and in the course textbook, so I will omit the details. Physics in Beware of the Spider involves collision detection of entities with walls and bullets, as well as collision detection between entities.

2.3.1 Collisions between Entities and Bullets Collisions between entities, and collisions between entities and bullets are checked in the same way. Entities and bullets have bounding spheres with a specific radius, tuned for

4D3 Computer Graphics 13 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.3 Physics and Collision Detection

the best gameplay. To check for a collision we trivially check if two bounding spheres intersect. As an example, Listing 4 contains a code snippet for checking collisions between spiders and bullets.

1 ... 2 foreach (var s in spiders[i]) 3 { 4 ... 5 6 // Check if spider has been hit with bullet 7 foreach (var b in bullets) 8 { 9 if (b.State == Bullet.States.Active && 10 s.State == Spider.States.Alive && 11 Geometry.SphereSphereIntersect(s.Pos, s.Radius, b.Pos, Bullet. Radius)) 12 { 13 b.State = Bullet.States.Destroyed; 14 s.HandleHit(); 15 continue; 16 } 17 } 18 ... 19 } Listing 4: Spider bullet collisions

2.3.2 Collision with Walls Entities check for collisions with Quads, two or four of which make up a wall, as dis- cussed in Section 2.2. It would have been incredibly wasteful to check for collisions against every single Quad at any given time, so Quads are grouped into different col- lections, each of which has an associated set of bounds. We can think of all the Quads in a collection as residing in a large rectangular “box”. Before checking for collisions with each Quad in this “box” we first check if we are actually inside the “box” itself, and if we are not then there can be no collision with any of the walls therein. This dramatically improves performance, even if the walls are split into no more than eight such collections, each with an associated set of bounds. If we are indeed within the set of bounds for a particular collection of Quads, then we can proceed to the actual collision detection. A plane is an infinite 2D subspace of the 3D world so a collision with a Quad is detected not only if the bounding sphere of the entity collides with the plane that the Quad is located on, but also if the bounding sphere is actually within the bounds for the Quad itself. The latter is computationally less demanding to check, so it is checked first. A sample of code implementing this logic to check for collisions in the xy-plane, is given in Listing 5 below. The same logic follows for collisions in the yz-plane.

4D3 Computer Graphics 14 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.3 Physics and Collision Detection

1 internal static bool CheckWallCollisions(Vector3f pos, float r) 2 { 3 for (int i = 0; i < WallBounds.Length; i++) 4 { 5 // Check if the position is within the bounds for this set of quads 6 bool inside = 7 pos.x > WallBounds[i].min.x && pos.z > WallBounds[i].min.z && 8 pos.x < WallBounds[i].max.x && pos.z < WallBounds[i].max.z; 9 10 // If not, then no need to check for collisions 11 if (!inside) 12 continue; 13 14 // Check XY collisions 15 foreach (var q in WallsXY[i]) 16 { 17 // The second vertices of the triangles in a 18 // quad are always on opposite sides 19 Vector3f a = q.triangles[0].vertices[1].p; 20 Vector3f b = q.triangles[1].vertices[1].p; 21 22 // Check if position is within the bounds for this quad 23 inside = (pos.x < a.x && pos.x > b.x) || (pos.x > a.x && pos.x < b.x); 24 if (inside) 25 { 26 Vector3f p = q.triangles[0].vertices[0].p; 27 Vector3f n = q.normal; 28 29 // Check intersect 30 if (Geometry.PlaneSphereIntersect(pos, r, p, n)) 31 { 32 // Collision detected 33 return true; 34 } 35 } 36 } 37 38 ... Listing 5: Checking for collisions with walls

To handle collisions with walls we first break up the velocity vector into two com- ponent vectors ~vx = (~vx, 0, 0) and ~vz = (0, 0,~vz). Using these two vectors, we obtain two projected positions, ~px = ~p + ~vx and ~pz = ~p + ~vz, by adding the respective com- ponent vector to the position vector. Then we independently check for collisions using the projected positions ~px and ~pz. If the projected positions result in a collision then we set the velocity component in that direction to zero. Checking for collisions in this way ensures that if the player collides with a wall in the x-direction, he is still able to “slide” along the wall in the z-direction, and vice versa2. Listing 6 below shows the

2This is the expected behavior in a first-person game

4D3 Computer Graphics 15 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.3 Physics and Collision Detection

C# code implementing this logic.

1 Vel.Normalize().Multiply(Speed); 2 3 // Handle wall collisions 4 Vector3f projX = Vector3f.Add(Pos, new Vector3f(Vel.x, 0, 0)); 5 bool collisionX = World.CheckWallCollisions(projX, Radius); 6 if (collisionX) 7 Vel.x = 0; 8 9 Vector3f projZ = Vector3f.Add(Pos, new Vector3f(0, 0, Vel.z)); 10 bool collisionZ = World.CheckWallCollisions(projZ, Radius); 11 if (collisionZ) 12 Vel.z = 0; 13 14 Pos.Add(Vel); Listing 6: Player collision handling

This example was for the player. Collision checking for spiders works in exactly the same way. Collision checking for bullets is even simpler. We check the bullet’s actual position, without worrying about projected positions since there is no danger of a bullet “getting stuck” at the point of collision – if a collision is detected, the bullet is simply removed.

2.3.3 Bullet Management When the player fires, a bullet is created and stored in a collection. Whenever a bullet hits a wall or an entity it is to be removed from the collection. You cannot remove from a collection during iteration (in C++ or C#) because it will invalidate the iterator. So the procedure requires two iterations, one pass to check if the bullet is to be destroyed and tag it accordingly and another pass to actually remove the destroyed bullets from the collection. Here is a typical C++ solution, assuming a simple method called CheckCollisions.

1 std::list::iterator iter = bullets.begin(); 2 while (iter != bullets.end()) 3 { 4 if (iter->CheckCollisions()) 5 iter->State = Bullet::States::Destroyed; 6 ++iter; 7 } 8 9 std::list updated; 10 iter = bullets.begin(); 11 while (iter != bullets.end()) 12 { 13 if (iter->State != Destroyed) 14 updated.push_back(*iter); 15 ++iter;

4D3 Computer Graphics 16 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.4 Models and Animation

16 } 17 18 bullets = updated; Listing 7: Removing destroyed bullets in C++

Compare that with the elegance and readability of a C# solution using LINQ, which does not require a temporary container.

1 foreach(var b in bullets) 2 if (b.CheckCollisions()) 3 b.State = States.Destroyed; 4 5 bullets = (from b in bullets 6 where b.State != Bullet.States.Destroyed 7 select b) 8 .ToList(); Listing 8: Removing destroyed bullets in C#

2.4 Models and Animation The game features four different enemies: big spiders, small spiders, eggs which hatch into baby spiders, and the Gargantuan Spider Queen (Spiders.cs). The four different spiders all inherit from the abstract base class Spider (Spider.cs). Both Spider and Player in turn inherit from Entity (Entity.cs), which contains basic physics fields and provides an interface for updating and drawing. The Spider class itself contains all the necessary logic for physics, behavior and animation that is shared by all spiders.

2.4.1 Hierarchical Spider Models Spiders are drawn using just two geometric primitives: the sphere and the cylinder. The body, the head and the eyes are formed with spheres. The legs are formed using cylinders, a sphere for the kneecap, and a cone for the lower leg (still a cylinder, with base width zero). GLUquadric objects were used to draw the primitives. Listing 9 contains the code for drawing a sphere. The code for the cylinder is similar.

1 protected static void DrawQuadricSphere(float radius, String textureKey) 2 { 3 Glu.GLUquadric quad; 4 Gl.glBindTexture(Gl.GL_TEXTURE_2D, Game.Textures[Game.TextureIndices[ textureKey]]); 5 quad = Glu.gluNewQuadric(); 6 Glu.gluQuadricTexture(quad, Gl.GL_TRUE); 7 Glu.gluSphere(quad, radius, 20, 20); 8 Gl.glBindTexture(Gl.GL_TEXTURE_2D, 0); 9 } Listing 9: Quadric sphere

4D3 Computer Graphics 17 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.4 Models and Animation

The spider model was created hierarchically, through trial and error. By starting with an oval for the body it was easy to visually position the head and the eyes and make the legs appear as natural as possible (Figures 8, 9). No modelling software was used to create the spiders – everything was done “by hand”. Once the basic spider model is drawn, it is stored in a display list and called whenever a spider needs to be drawn. Different spiders differ in scale, texture and animation parameters but aside from that the underlying hierarchical model is the same. The code for drawing the spider is contained in DrawSpiderAlive and DrawSpiderDead in Spider.cs.

Figure 8: Big spider

Perhaps the biggest optimization step in the game concerns the management of display lists between the different types of spiders. Ideally, we would like to have a single display list for each spider type. This is achieved easily by declaring a static display list for the Spider class which is shared by all instances. But with this approach, we encounter a problem since the derived classes use different scales and textures so they cannot share the same display lists. The solution once again owes to the elegance and simplicity of C#, using a language feature known as the static constructor. A static constructor is a method that is called once per class, rather than once per instance, and is guaranteed to be called before the first instance of the class is created.

4D3 Computer Graphics 18 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.4 Models and Animation

Figure 9: Small spider

The logic is as follows. The Spider base class has a member handle to a display list and a generic method for generating display lists (Listing 10).

1 // Texture keys 2 protected static String TextureKey; 3 4 ... 5 6 // Display lists 7 protected int[] DisplayListsAlive = new int[40]; 8 protected int DisplayListDead; 9 protected int DisplayListBlood; 10 11 ... 12 13 protected static void CreateDisplayLists( 14 ref int[] displayListAlive, 15 ref int displayListDead, 16 ref int displayListBlood, 17 float animationFrameFactor, 18 float animationAngleFactor, 19 float animationSpeedFactor 20 {

4D3 Computer Graphics 19 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.4 Models and Animation

21 ... 22 } Listing 10: Base class member display list handles and a base class method for creating display lists

Note that the fields are unassigned and protected, and that CreateDisplayLists takes arguments by reference. The derived spiders all have a set of static display lists. When a spider is created (for example, the big spider), the static constructor is the first method called and it generates a single set of static display lists for all big spiders using the base class static method CreateDisplayLists (Listing 11).

1 // Display lists 2 static int[] StaticDisplayListsAlive = new int[40]; 3 static int StaticDisplayListDead; 4 static int StaticDisplayListBlood; 5 ... 6 7 static SpiderBig() 8 { 9 // Assign texture key 10 TextureKey = "spider1"; 11 12 // Generate display lists 13 CreateDisplayLists( 14 ref StaticDisplayListsAlive, 15 ref StaticDisplayListDead, 16 ref StaticDisplayListBlood, 17 2, 0.5f, 4); 18 } Listing 11: Big spider static constructor and static display list handles Note that the unassigned base class texture key is now assigned according to the derived class, and it is the static display lists that are passed to CreateDisplayLists. Finally, the next method invoked is the instance constructor, which binds the in- stance display lists in the base class to the static display lists which have been created in the derived class static constructor (Listing 12).

1 internal SpiderBig(Vector3f pos, Vector3f playerPos, float playerRadius) 2 : base(pos, playerPos, playerRadius) 3 { 4 ... 5 6 // Bind display lists 7 DisplayListsAlive = SpiderBig.StaticDisplayListsAlive; 8 DisplayListDead = SpiderBig.StaticDisplayListDead; 9 DisplayListBlood = SpiderBig.StaticDisplayListBlood; 10 } Listing 12: Big spider instance constructor

4D3 Computer Graphics 20 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.4 Models and Animation

The next time a big spider is created only the instance constructor is called, which simply binds to the static display lists already in memory. So in summary, each spider instance has a handle to a set of display lists, but there are only four actual sets of display lists ever created – one for each spider type, generated by the static constructor of that class. At the same time, there is no loss of generality as we are able to make use of a single base class method for creating the display lists. The end result is a very efficient use of resources and the ability to create many spiders of different types.

2.4.2 Animation There are two kinds of animation in the game: leg animation and blood animation. When the spider is alive, he moves around and the legs animate to reflect the movement. When the spider dies, he lies stretched out and a pool of blood slowly appears around him. All animation is pre-determined and stored in display lists before the spiders are ever created, as already discussed. For leg animation, this is done by having an array of display lists, rather than a single display list. The array is dynamically indexed through by a global frame counter incremented in glutIdleCallback, the main update loop. Furthermore, the last frame in the animation sequence is designed to match the first frame, which makes it look like the spider is constantly moving its legs. The Spider class has 12 different angles and 2 additional flags and for each of the 40 frames in the leg animation sequence, the angles are pre-calculated using the current frame number and three additional parameters. It is these parameters (frame factor, angle factor and speed factor) which allow the same method to create different animation sequences for different spider types. Once the angles are calculated, the current frame is drawn using those angles and stored in the display list array. The angles control 3 different points of rotation for each leg, with different delays between them, and make it look like the spider is walking around. When the display lists are generated we can simply call them using the current frame number modulo 40, to create a smooth and continuous animation sequence. Listing 13 contains the code for generating the leg angles. The logic is very ad-hoc and difficult to explain. It took hours to write, and it works just because it works.

1 protected static void CalculateAngles(long frame, 2 float animationFrameFactor, float animationAngleFactor, float animationSpeedFactor) 3 { 4 l1inc = 30 + (float)(frame % (5 * animationSpeedFactor)) * animationAngleFactor; 5 l2inc = 45 - (float)(frame * 2 % (10 * animationSpeedFactor)) * animationAngleFactor; 6 l3inc = 00 + (float)(frame % (5 * animationSpeedFactor)) * animationAngleFactor; 7 l1dec = 40 - (float)(frame % (5 * animationSpeedFactor)) * animationAngleFactor;

4D3 Computer Graphics 21 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.4 Models and Animation

8 l2dec = 25 + (float)(frame * 2 % (10 * animationSpeedFactor)) * animationAngleFactor; 9 l3dec = 10 - (float)(frame % (5 * animationSpeedFactor)) * animationAngleFactor; 10 11 r1inc = 30 + (float)((frame + (5 * animationFrameFactor)) % (5 * animationSpeedFactor)) * animationAngleFactor; 12 r2inc = 45 - (float)((frame + (5 * animationFrameFactor)) * 2 % (10 * animationSpeedFactor)) * animationAngleFactor; 13 r3inc = 00 + (float)((frame + (5 * animationFrameFactor)) % (5 * animationSpeedFactor)) * animationAngleFactor; 14 r1dec = 40 - (float)((frame + (5 * animationFrameFactor)) % (5 * animationSpeedFactor)) * animationAngleFactor; 15 r2dec = 25 + (float)((frame + (5 * animationFrameFactor)) * 2 % (10 * animationSpeedFactor)) * animationAngleFactor; 16 r3dec = 10 - (float)((frame + (5 * animationFrameFactor)) % (5 * animationSpeedFactor)) * animationAngleFactor; 17 18 swapl = (frame % (10 * animationSpeedFactor)) < (5 * animationSpeedFactor); 19 swapr = (frame + (5 * animationFrameFactor)) % (10 * animationSpeedFactor) < (5 * animationSpeedFactor); 20 } Listing 13: Generating animation angles

Once we have the angles for the current frame, we draw the legs in DrawSpiderAlive (Listing 14). The code below shows just one side of the body. The swap flags are checked before deciding which angle to use and determine whether the leg is moving “forwards” or “backwards”. This is essentially what gives rise to the pendulum-like motion.

1 #region LEGS 2 Gl.glPushMatrix(); 3 { 4 Gl.glRotatef((swapl ? l1inc : l1dec) / 2 + 5, 0, 0, 1); 5 6 Gl.glRotatef((swapl ? l1inc : l1dec), 0, 1, 0); 7 DrawLeg(85 + (swapl ? l3inc : l3dec), 2, TextureKey); 8 9 Gl.glRotatef((swapl ? l2inc : l2dec), 0, 1, 0); 10 DrawLeg(95 + (swapr ? r3inc : r3dec), 2.2f, TextureKey); 11 Gl.glRotatef((swapl ? l3inc : l3dec), 0, 1, 0); 12 13 Gl.glRotatef((swapr ? r1inc : r1dec), 0, 1, 0); 14 DrawLeg(95 + (swapl ? l3inc : l3dec), 2.2f, TextureKey); 15 16 Gl.glRotatef((swapr ? r2inc : r2dec), 0, 1, 0); 17 DrawLeg(85 + (swapr ? r3inc : r3dec), 2.1f, TextureKey); 18 } 19 Gl.glPopMatrix(); 20 #endregion LEGS Listing 14: Drawing the spider legs using animation angles

4D3 Computer Graphics 22 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.4 Models and Animation

When a spider is killed, he is drawn as an inanimate carcass. The dead spider is drawn the same way as the living spider, but with different angles for the legs and a different height for the body. Again, this was determined through trial and error to achieve the most convincing results. The second aspect of animation is to draw what looks like an expanding pool of blood coagulating around the dead spider. Although it may look convincing (Figures 10, 11), the animated blood is actually somewhat of a hack. The effect is achieved by rendering seven spheres below the ground and draping them with a blood texture. Then by raising these spheres slowly we expose their tops above ground level, making it look like a pool of thick blood. The spheres are stored in a single display list and are not animated as such. The animation effect is achieved solely through changing the position of the spheres with time.

Figure 10: Dead spider carcass lying in a pool of blood

2.4.3 Animated Sky Lastly, it is worth mentioning the animated sky. Although better looking methods exist which construct a dome from individual vertices, using a sphere proved very simple and looks great. The sphere is created using a GLUquadric, textured, and scaled to fit around the world model (Listing 15). The sky is then stored in a display list and rotated according to the current frame to create the effect of moving clouds.

4D3 Computer Graphics 23 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.4 Models and Animation

Figure 11: Some more arachnid cadavers...

1 static void DrawSky() 2 { 3 Gl.glPushMatrix(); 4 { 5 Gl.glBindTexture(Gl.GL_TEXTURE_2D, Game.Textures[Game.TextureIndices [TextureKeySky]]); 6 Glu.GLUquadric sky = Glu.gluNewQuadric(); 7 Glu.gluQuadricTexture(sky, Gl.GL_TRUE); 8 double radius = (double)Math.Min((decimal)MazeWidth, (decimal) MazeHeight) * ModelScale; 9 Glu.gluSphere(sky, radius, 10, 10); 10 Gl.glBindTexture(Gl.GL_TEXTURE_2D, 0); 11 } 12 Gl.glPopMatrix(); 13 } Listing 15: Sky sphere

4D3 Computer Graphics 24 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.5 Gameplay and AI

2.5 Gameplay and AI 2.5.1 Spider Bite Handling The player must somehow handle being bitten by a spider. In terms of implementation, the spider knows nothing of the player and the player knows nothing of the spider but the two must somehow manage this situation. In C++ this would typically be handled in a top level module, with both classes providing some form of public interface. In C# this can be implemented quite tersely using events and event handlers. The Spider class declares an event which is triggered when a Spider object comes into contact with the player.

internal delegate void SpiderBiteDelegate(float damage); internal static event SpiderBiteDelegate SpiderBite;

The Player class subscribes to the event with an event handler, the signature for which is defined by the delegate.

Spider.SpiderBite += new Spider.SpiderBiteDelegate(onSpiderBite);

Whenever the spider detects a collision with the player it fires the SpiderBite event, by simply calling it as a method.

SpiderBite(Spider.Damage);

The player will handle the event being fired with a call to the method onSpiderBite. Everything happens automatically and no top-level logic is involved here. All lateral interaction between classes is handled by those classes. Furthermore, encapsulation is preserved perfectly as, despite Spider declaring the event as internal, the only action other classes can take is to subscribe or unsubscribe from it, and no data is ever exposed unnecessarily.

2.5.2 Spider AI Spiders are initially asleep but they have a sense of smell. Internally, “smell” is a vector from the spider’s position to the player’s position (~s = ~pplayer − ~pspider). A spider will wake up when it has direct sight of the player. This is established with the aid of a method in World.cs called DistanceToNearestWall. This method takes a start point (the spider’s position), an end point (the player’s position) and a direction vector (the “smell” vector) and determines whether the direction vector traced from the start position in the positive orientation intersects a wall before it reaches the end position, and if so, what the distance is to the nearest wall it intersects. To achieve this, the method uses the Geometry function RayPlaneIntersect, which has already been introduced in Section 2.3. Because the walls being checked are only those bounded within the rectangle formed by the start and end positions at its diagonals, the method is very efficient. Having calculated this distance dmin, we simply check if |~s| < dmin. If this is the case then we conclude that the smell vector does not intersect any walls

4D3 Computer Graphics 25 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.6 Stereo Display Mode

(i.e. there are no walls between the spider and the player), and therefore the spider has spotted the player and will awaken. Once awakened, the spider will pursue you by following your smell. If the spider is big and slow, it is possible to run away and hide behind a wall, for example. In that case, the spider will move around randomly until it sees you again. However, if a small but fast spider spots you, it will almost certainly not be possible to escape and the spider will give chase until one of you is dead. Introducing this basic form of AI not only makes the game that little bit more interesting, but also serves as a major performance safeguard. While spiders are asleep they do not move, and consequently they do not need to check for collisions or update any physics. By the time you succeed in awaking more than just a couple of spiders, some of them (or you!) will already be dead so at no point are there more than just a few spiders being actively updated. The logic for spider AI is contained within the Update method in Spider.cs.

2.5.3 Spider Eggs

The class SpiderBaby adds another special feature to the game. Spider babies are initially lying around in the form of unhatched eggs. Eggs are randomly positioned within their vicinity and given random rotations as well as ochre yellow eggshell textures to make them look realistic (Figure 12). If you shoot or step on an egg, the spider baby will hatch and start attacking you. This can be quite scary and entertaining! The logic for unhatched spider babies is made possible through polymorphic over- rides of the Update and Draw methods defined in the SpiderBaby class. Once a spider baby is hatched, it will revert to the base class calls and behave just like a normal spider.

2.6 Stereo Display Mode Beware of the Spider features an anaglyph stereo mode, allowing it to be viewed in 3D. The idea behind stereo is quite simple – use two cameras, instead of one, which simulate two human eyes, and then somehow make the left eye see the left view and the right eye see the right view. Many new techniques for doing this have emerged as stereoscopic technology becomes increasingly popular, but none are as old-school as red-cyan anaglyph! The correct way to create two stereo views is to align the two cameras parallel to each other (Figure 13). We cannot simply rotate one camera into the position of the other one, subtending an arc, as despite also appearing stereoscopic, this will introduce vertical disparity which becomes more pronounced near the edges of the scene causing visual discomfort for the viewer. The perpendicular distance between the two cameras is called the interocular dis- tance and should correspond to the distance between the human eyes. The focal length (which must be the same for both cameras) determines the point of no parallax in the stereo scene. At this point the two views coincide, essentially creating a mono view,

4D3 Computer Graphics 26 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.6 Stereo Display Mode

Figure 12: Spider eggs and it this plane that the viewer perceives to be at the same depth as the surface of the screen. The stereoscopic effect is achieved through positive and negative parallax occurring on planes in front or behind the plane of no parallax. By controlling the focal length of the cameras we control how much of the scene is perceived to be “going into” the screen and how much “coming out of” the screen. Finding the right focal length is in many ways an art form and comes down to personal preference. Things coming out of the screen are more pronounced and more striking, but as a general rule we should avoid objects coming out of the screen if they are not wholly within the physical frame bounding the screen itself, as this creates a visual paradox since the frame of the screen is the plane of no parallax. Most of the time the walls will be cut by the edges of the screen, so most, if not all, of the scene should be going into the screen. Figure 14 contains a screenshot of the game in stereo mode. The towers illustrate these concepts particularly well. The parallax of the tower which is further back is more pronounced than that of the towers which are closer. The implementation of the stereo views is as follows. First we calculate the right vector by taking the cross product of the player’s view direction and up vector (~r = d~ × ~u). Then we translate the player’s actual position into the position of the two δ  δ  cameras to obtain ~pl = ~p − ~r × 2 and ~pr = ~p + ~r × 2 , where δ is the interocular

4D3 Computer Graphics 27 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.6 Stereo Display Mode

Figure 13: Stereo projection distance. Now we can create the stereo view by drawing the same scene at each position. The theory behind producing correct anaglyph images is actually quite involved. If ~a is a pixel in the anaglyph image and ~l and ~r are pixels in the left and right images, respectively, then

~ ~a = SL l + SR ~r (1)

where SL and SR are 3×3 transformation matrices. If we now consider this operation 6 3 to be a map S : R → R then we can reduce (1) to a single linear operation

 ~l  ~a =  S S  (2) L R ~r  ~l  = B (3) ~r

which is fully parametrized by the 3 × 6 matrix B. This is the canonical form of the anaglyph algorithm. Different anaglyph techniques differ only in the transformation matrix B used, and some are extremely complicated but the simplest method known as the Photoshop algorithm is quite easy to implement and works very well. The red channel of the left image is combined with the blue and green channels of the right image by simple addition so that

 1 0 0 0 0 0  B =  0 0 0 0 1 0  (4) 0 0 0 0 0 1

4D3 Computer Graphics 28 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.6 Stereo Display Mode

Figure 14: Screenshot of the game in stereo mode

In OpenGL this is achieved quite easily by using the function glColorMask to turn the required channels on and off. Listing 16 contains the code for creating a stereo scene and displaying it as an anaglyph using the Photoshop algorithm.

1 void DisplayCallback() 2 { 3 // Stereo projection maths 4 player.Dir.Normalize(); 5 Vector3f focus = Vector3f.Add(player.Pos, Vector3f.Multiply(player.Dir, FocalLength)); 6 Vector3f right = Vector3f.Cross(player.Dir, player.Up).Normalize(); 7 right.Multiply(IOD).Divide(2.0f); 8 9 // Clear color and depth buffers 10 Gl.glClear(Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT); 11 12 // Reset color mask 13 Gl.glColorMask(Gl.GL_TRUE, Gl.GL_TRUE, Gl.GL_TRUE, Gl.GL_TRUE); 14 15 #region LEFT VIEW

4D3 Computer Graphics 29 Mikhail Volkov OpenGL Project Report 2 IMPLEMENTATION DETAILS 2.6 Stereo Display Mode

16 17 // Apply left view projection 18 Gl.glMatrixMode(Gl.GL_PROJECTION); 19 Gl.glLoadIdentity(); 20 Glu.gluPerspective(Aperture, (double)WindowWidth / (double)WindowHeight, 0.1, 1000); 21 Glu.gluLookAt( 22 player.Pos.x - right.x, player.Pos.y - right.y, player.Pos.z - right .z, 23 focus.x, focus.y, focus.z, 24 player.Up.x, player.Up.y, player.Up.z); 25 26 // Draw sky in mono using the left view 27 Gl.glMatrixMode(Gl.GL_MODELVIEW); 28 World.DrawMono(); 29 30 // Apply the red filter 31 Gl.glColorMask(Gl.GL_TRUE, Gl.GL_FALSE, Gl.GL_FALSE, Gl.GL_TRUE); 32 33 // Draw the left view 34 Gl.glClear(Gl.GL_DEPTH_BUFFER_BIT); 35 Gl.glMatrixMode(Gl.GL_MODELVIEW); 36 37 ... 38 39 #endregion LEFT VIEW 40 41 #region RIGHT VIEW 42 43 // Apply right view projection 44 Gl.glMatrixMode(Gl.GL_PROJECTION); 45 Gl.glLoadIdentity(); 46 Glu.gluPerspective(Aperture, (double)WindowWidth / (double)WindowHeight, 0.1, 1000); 47 Glu.gluLookAt( 48 player.Pos.x + right.x, player.Pos.y + right.y, player.Pos.z + right .z, 49 focus.x, focus.y, focus.z, 50 player.Up.x, player.Up.y, player.Up.z); 51 52 // Apply the cyan filter 53 Gl.glColorMask(Gl.GL_FALSE, Gl.GL_TRUE, Gl.GL_TRUE, Gl.GL_TRUE); 54 55 // Draw the right view 56 Gl.glClear(Gl.GL_DEPTH_BUFFER_BIT); 57 Gl.glMatrixMode(Gl.GL_MODELVIEW); 58 59 ... 60 61 #endregion RIGHT VIEW 62 63 // Reset color mask 64 Gl.glColorMask(Gl.GL_TRUE, Gl.GL_TRUE, Gl.GL_TRUE, Gl.GL_TRUE);

4D3 Computer Graphics 30 Mikhail Volkov OpenGL Project Report 4 FINAL REMARKS

65 66 ... 67 68 // Redraw display 69 Glut.glutPostRedisplay(); 70 Glut.glutSwapBuffers(); 71 } Listing 16: Anaglyph stereo view using the Photoshop algorithm

The actual code for drawing the models and the code for drawing a mono view have been omitted for brevity. Notice that since the sky is very far away it only needs to be drawn in mono. It would have been more correct to translate to the center to draw the sky, but it is more efficient to simply draw it using one of the two stereo views, and the difference is unnoticeable.

3 Sources

Most of the textures came from the Shoot-em-up Project (http://shoot.sourceforge. net) and some from Google image searches. All source code is entirely my own work.

4 Final Remarks

Making this game was a lot of fun. I tried to touch on most of the major issues pertinent to 3D game design. In particular, I enjoyed implementing mouse navigation from first principles, as we studied spherical coordinates for two years and this was the first time I was able to see it applied in a meaningful context. In the end I also was surprised at how well C# coped in terms of performance. I don’t know how much faster this game would have turned out in C++, but in general I think .NET compares quite favourably. Although I set off intent on overcoming my fear of spiders, it is sure to backfire as staying up too long writing code often causes residual nightmares...

4D3 Computer Graphics 31 Mikhail Volkov OpenGL Project Report