This tutorial's art assets:

This tutorial is accurate as of Release 0.4.

Step 1

Download the most current release. Create a new Windows Game 3.0 XNA project.

Step 2

Add a reference to XNAQ3Lib.Q3BSP.dll and XNAQ3Lib.MD5.dll to the Game project.
Add a reference to Q3BSPContentPipelineExtension and MD5ContentPipelineExtension to the Content project.
To do this, right click on the projects, select the browse tab, and find the .dlls you downloaded on the release page. Alternatively, download the source code for the two projects and add them to the solution (right click on the solution and select Add -> Existing Project), then add the references from the Project tab of Add Reference.

Step 3

Add the following code to allow (very) basic 3D display and movement.
  • Add the following to the Using Statements:
using XNAQ3Lib.MD5;
using XNAQ3Lib.Q3BSP;
  • Add these to the member variables of the Game class:
Matrix view;
Matrix projection;

Vector3 cameraPosition = Vector3.Zero;
Vector3 cameraAngle = Vector3.Zero;
Vector3 cameraForward = Vector3.Forward;
Vector3 cameraLeft = Vector3.Left;
KeyboardState lastKeyboardState;
MouseState lastMouseState;

Q3BSPLevel level;
  • Add the following lines to Game.Initialize()
projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(90), 4.0f / 3.0f, 0.1f, 10000.0f);
cameraPosition = new Vector3(0, 10, 0);
  • Add the following lines to Game.Update()
const float speed = 200.0f;
const float rotSpeed = 20.0f;

KeyboardState keyboardState = Keyboard.GetState();
MouseState mouseState = Mouse.GetState();

// Allows the game to exit
if (keyboardState.IsKeyDown(Keys.Escape))

cameraAngle.X += MathHelper.ToRadians((mouseState.Y - this.Window.ClientBounds.Y / 2) * rotSpeed * 0.01f); // pitch
cameraAngle.Y += MathHelper.ToRadians((mouseState.X - this.Window.ClientBounds.X / 2) * rotSpeed * 0.01f); // yaw

cameraForward = Vector3.Normalize(new Vector3((float)Math.Sin(-cameraAngle.Y), (float)Math.Sin(cameraAngle.X), (float)Math.Cos(-cameraAngle.Y)));
cameraLeft = Vector3.Normalize(new Vector3((float)Math.Cos(cameraAngle.Y), 0f, (float)Math.Sin(cameraAngle.Y)));

if (keyboardState.IsKeyDown(Keys.W))
    cameraPosition -= speed * (float)gameTime.ElapsedGameTime.TotalSeconds * cameraForward;
if (keyboardState.IsKeyDown(Keys.S))
    cameraPosition += speed * (float)gameTime.ElapsedGameTime.TotalSeconds * cameraForward;

if (keyboardState.IsKeyDown(Keys.A))
    cameraPosition -= speed * (float)gameTime.ElapsedGameTime.TotalSeconds * cameraLeft;
if (keyboardState.IsKeyDown(Keys.D))
    cameraPosition += speed * (float)gameTime.ElapsedGameTime.TotalSeconds * cameraLeft;

if (keyboardState.IsKeyDown(Keys.LeftShift))
    cameraPosition.Y += speed * (float)gameTime.ElapsedGameTime.TotalSeconds;
if (keyboardState.IsKeyDown(Keys.LeftControl))
    cameraPosition.Y -= speed * (float)gameTime.ElapsedGameTime.TotalSeconds;

view = Matrix.Identity;
view *= Matrix.CreateTranslation(-cameraPosition);
view *= Matrix.CreateRotationY(cameraAngle.Y);
view *= Matrix.CreateRotationX(cameraAngle.X);   
if (this.IsActive)
    Mouse.SetPosition(this.Window.ClientBounds.X / 2, this.Window.ClientBounds.Y / 2);

Step 4

Add the art assets to the project.
  • Add basicQ3BSPEffect.fx to the root of the Content project.
  • In order for the BSP to find it's textures, the folder structure needs to be preserved. If you're not sure what the exact file structure, this library writes a file called "log.txt" when loading a level in debug mode. In this file is a listing of every texture used by the loaded BSP. Add the textures to the content project in the folder "textures\xnaq3lib\".
  • Quake 3 uses text files with the extension .shader for all textures more advanced than a simple color map. A complete explanation of the scripting can be found here. You can put these files anywhere, as long as all the .shaders you use are in the same place and the location is fed to Q3BSPLevel.InitializeLevel(). Add xnaq3lib.shader to the folder "shaders\".
  • The BSP file itself is not yet loaded by the Content Pipeline. Add this file to the root of the game project, not the content project. You will have to change its properties to "Build Action: None" and "Copy to Output Directory: Always copy."

Step 5

  • Add the following lines to Game.LoadContent(). If you put the .shader file in a different directory you'll have to specify it in Q3BSPLevel.InitializeLevel.
level = new Q3BSPLevel(Q3BSPRenderType.StaticBuffer);
level.BasePath = "xnaq3lib_test";

if (level.LoadFromFile(level.BasePath + ".bsp"))
    bool levelLoaded = level.InitializeLevel(GraphicsDevice, Content, @"shaders\");
  • Add the following line to Game.Draw()
level.RenderLevel(cameraPosition, view, projection, gameTime, GraphicsDevice);

Step 6

  • Build the program. You should see something close to the following output:

The completed tutorial for this project can be found here:

Last edited Feb 8, 2009 at 7:21 AM by MasterSlowPoke, version 12


Traveller Feb 21, 2009 at 10:10 AM 
When I unzipped and ran the zipped tutorial at the bottom of the page, I get this error on line 154, level.RenderLevel(cameraPosition, view, projection, gameTime, GraphicsDevice);

"InvalidOperationException was unhandled
Both a valid vertex shader and pixel shader (or valid effect) must be set on the device before any draw operations may be performed."

The above error also happens with the precompiled exe in there. I'm on Microsoft Visual C# 2008 Express, XNA 3.0

My video card isn't very recent--Apparently it supports pixel shader v1.4 and vertex shader v1.1. But video cards seven years old are NOT that uncommon...seriously, this machine ran Quake 3 just fine, it really seems like a library that renders Q3 maps should be able to degrade gracefully? I don't need advanced shaders...