Let the 3D Rendering Start (XNA Game Studio 4.0 Programming) Part 1

Now that we covered the basics of the graphics pipeline and some of the math that is involved, let’s see how these concepts relate to the types exposed by XNA Game Studio and draw some triangles on the screen.

GraphicsAdapter

Use the GraphicsAdapter class to enumerate and update graphics adapters. Most single monitor PCs have one graphics adapter that represents their physical graphics card and the one connection it has to the monitor. Some graphics cards provide two graphics adapters so you can plug two monitors into a single graphics card. In this case, the GraphicsAdapter allows for selecting two different adapters so you can display the graphics on two different monitors.The Xbox 360 and Windows Phone devices provide only one GraphicsAdapter.

The static GraphicsAdapter.DefaultAdapter property returns the default adapter in which there are multiple adapters to chose from. For example, when you set the left monitor to be the primary display in Windows, the DefaultAdapter property returns the GraphicsAdapter to display graphics on the left monitor.

The static GraphicsAdapter.Adapters property returns a read-only collection of the adapters available.You rarely need to enumerate graphics adapters. Most commercial PC games provide a settings page that enables the user to select the adapter so he or she can change which display to view the game. This is required when the game runs in full screen and the user does not have the normal window controls to click and drag.


If your game uses windowed mode, the user can click and draw the window to the other monitor.Although this changes the GraphicsAdapter used, all of this is handled for the user by XNA Game Studio by default.

Note

If you developed games using a native graphics API before and tried to support monitor dragging and toggling between windowed and full-screen modes, you know the trouble to get it working properly.

The GraphicsAdapter provides several properties and methods that enable you to determine what type of graphics card is available and what the capabilities of the graphics card are.

The QueryBackBufferFormat and QueryRenderTargetFormat methods can be used to determine whether a specific back buffer or render target format is supported.We discuss back buffers and render targets later in this topic.

The DeviceName, Description, DeviceId, VendorId, SubSystemId, and Revision properties are used to return information about a specific GraphicsAdapter to determine whether the graphics card is a specific model from a specific manufacturer. They also enable you to determine what the current driver version is.

Note

A graphics driver is a piece of software that enables the operating system to communicate with the graphics hardware. Graphics card manufacturers create the driver to instruct the operating system on how to perform specific tasks with the hardware.

Each GraphicsAdapter can support a number of display modes. Just as you can set your PC to use different resolutions, your game can use different resolutions. The SupportedDisplayModes returns a collection of DisplayMode structures. Each structure contains properties for the Width, Height, and Format for the display mode.A display format is defied using the SurfaceFormat enumeration.The format defines how each pixel on the screen is represented in terms of the data that is stored at each pixel.The most common format, SurfaceFormat.Color, is a 32-bit value that stores four channels of red, green, blue, and the alpha each with 8 bits of value.

Note

Alpha is a term used to express how transparent a color is.

The DisplayMode structure also provides two helper properties called AspectRatio and TitleSafeArea.The AspectRatio is the value you get when you divide the width of the display by the height of the display.This is often calculated when working with 3D graphics and is sometimes calculated incorrectly. The property gives you a simple way to retrieve the value without having to calculate the value each time you need it.

When drawing to televisions, they have outer edges of the screen cut off. If you draw to the top left corner of the screen and display this on a number of televisions, you will notice that each television has a slightly different position of where the top left corner is. The TitleSafeArea defines where you should limit drawing game specific graphics such as text and the heads up display for a character. Using the property ensures that your graphics are visible across different televisions.

GraphicsDevice

The GraphicsDevice in your game is responsible for issuing the drawing commands down to the driver, which then goes through the graphics pipeline.The GraphicsDevice is also responsible for loading graphics resources such as textures and shaders.

Every GraphicsDevice contains a back buffer.The back buffer is the data buffer where the graphics that ultimately end up displayed on your monitor are drawn to. It is called the back buffer because it is not displayed on the screen.What you see displayed on the screen is actually the contents of what is called the front buffer. Drawing occurs to the back buffer, so you don’t see each piece of geometry appear on the screen as it is drawn.After all of the draw calls are performed in a single frame, the GraphicsDevice.Present method is called, which flips the front and back buffers to display the contents of the back buffer.

Along with a width and height, the back buffer also has a SurfaceFormat, which defines the bit layout and use for each pixel in the back buffer. As we discussed, the GraphicsAdapter can be used to determine what formats are available.The most common is the Color format.

If a back buffer resolution that is smaller than the display is requested on the Xbox 360 or on a Windows Phone, the final image is up scaled to fit the screen. If the aspect ratio is different, then the display black bars are displayed on the top and bottom of the screen called letterboxing or on the sides of the screen called pillarboxing to fill out the extra space.

When calling the Present method, you specify a PresentInterval.The present interval is used to determine when the flip of the front and back buffers occur. Displays on computer monitors, televisions, and phone screens all have a refresh rate.The refresh rate is the speed that the displays update their physical screen commonly between 30Hrz to 60Hrz.When displays update, they do so by updating line by line until the entire screen is updated. At that time, the display performs a vertical retrace sometimes referred to as a vblank.This is when the screen is moving from the end of where it is currently updating to the start again. If the front and back buffers are flipped in the middle of the display, updating a graphical artifact called tearing can occur.Tearing is where part of the displayed screen shows one frame while another portion displays another.This occurs if the flip is not in sync with the vertical retrace because you change what is drawn on the screen in the middle of the display updating the display. To prevent this, the flip should occur during the vertical retrace.

When you set the PresentInterval, you have three options of when the flip between the front and back buffers occurs. If PresentInterval.One is used, the buffers wait to flip until the display is in the vertical retrace phase. This means the fastest your graphics can update is the same speed as your display’s refresh rate. PresentInterval.Two is used only to flip the buffers every other vertical retrace. PresentInterval.Immediate can be used to perform the flip as soon as possible and not wait for the vertical retrace. Although your game can suffer from tearing artifacts, this mode is helpful when performing performance analysis because it does not limit the speed at which your game can draw.

Along with the back buffer, a GraphicsDevice can also contain another buffer called the depth buffer.The depth buffer is used to store the depth values of a triangle when the color of that triangle is written to the back buffer.The values are used in the depth test position of the graphics pipeline that we discussed previously. A depth buffer can have three types of formats specified by the DepthFormat enumeration.The first is Depth16, which stores a 16-bit floating point value at each pixel. Depth24 provides a 24-bit floating point number to store depth at each pixel. Finally, the Depth24Stencil8 format provides the same 24 bits for depth but also enables another 8 bits to be used by the stencil buffer. The stencil buffer is used in the stencil test portion of the graphics pipeline.

Creating the GraphicsDevice

Although it is possible to create the graphics device yourself using the GraphicsDevice constructor, this is not needed when using Game class because the default template creates a GraphicsDeviceManager, which creates the GraphicsDevice.

If you want to change any of the default values that the GraphicsDeviceManager uses to create the graphics device, you can use code similar to the following in your Game class constructor.

tmpD-74_thumb

The GraphicsDeviceManager.SupportedOrientations can be used to define which orientations you want your game to support. This is useful for Windows Phone games where the user can flip the physical phone device and might do so because of the requirements of the game. If you elect to support an orientation when the user rotates the phone, the screen flips to support the new orientation. For example, if you want to support a landscape game where the user can play the game by rotating the phone to the left or right, use the following lines of code:

tmpD-75_thumb

Now when the user rotates the phone from one horizontal position to the other, the display automatically flips and your game appears correctly for the new orientation.

Reference Devices

There is a special type of graphics device called a reference device also known as ref device.A reference device does not use the graphics hardware and implements all of the graphics functionally in software on the computer’s CPU. A reference device can be used when the graphics hardware does not support some specific graphics feature. The downside is that the reference device is tremendously slow compared to graphics hardware.

Drawing with Primitives

If you have never developed 3D graphics applications, this is where you become a graphics developer for the first time.All of the geometry that we draw in future topics builds upon the following primitive drawing.

Primitive Types

XNA Game Studio supports four types of primitives defined by the PrimitiveType enumeration: TriangleList, TriangleStrip, LineList, and LineStrip.A TriangleList is a list of triangles just like its name implies. Every three vertices are treated as a new triangle meaning that each triangle requires three vertices to be stored. In most cases, this is overkill because most triangles are directly next to another triangle.With a TriangleStrip, the last three vertices are used to draw the next triangle. For example, you can draw two triangles with only four vertices. The first three vertices make up the first triangle and the last vertex is used in conjunction with the last two vertices from the first triangle to form the second triangle. Using triangle strips helps reduce the amount of memory required to store the vertices of triangles.The TriangleStrip is by far the most common PrimitiveType used.

The ListList is similar to the triangle list except that only two points are needed to form a single line. Every two points creates a new line segment. The LineStrip is similar to a triangle strip except that only the last point from the previous line segment is needed to form another line.

Vertex Types

Each vertex that makes up the triangle contains the position data for where the triangle is in space, but it can also contain other data such as the color that the triangle should be, texture coordinates for textured triangles, or normal data for lighting calculations. XNA Game Studio provides several ways to build in vertex types that can be used when defining your triangles or lines.You can also create your own vertex types for more complex rendering scenarios.

Drawing Primitives

There are ways to render geometric primitives in XNA Game Studio. All four methods are provided by the GraphicsDevice class.The first two methods—DrawUserPrimitives and DrawUserIndexedPrimitives—both work by specifying an array of vertices and a primitive type to the GraphicsDevice, which, in turn, draws the primitives.

The second two methods—DrawPrimitives and DrawIndexedPrimitives—use vertices that have been stored on the graphics hardware. Storing vertex data directly on the graphics hardware lowers drawing latency because the data needed to draw the geometry is already in memory in the graphics hardware near where the computations are occurring.

Along with vertex data, the two indexed methods—DrawUserIndexedPrimitives and DrawIndexedPrimitives—also use index values that are used to index into the vertex data. Instead of using the vertex data directly to draw the primitive, indexed vertices are defined using an offset into the vertex data. This saves space when multiple primitives use the same vertex.

DrawUserPrimitives

The easiest way to draw a primitive on the screen is to declare an array of vertices and pass these to the DrawUserPrimitive method.Along with the vertices, we create an instance of BasicEffect.For now, you know that the effect is needed so the graphics hardware knows what to do in the vertex shader and pixel shader portions of the graphics pipeline.

Define the following member variables in your Game class:

tmpD-76_thumb

Next, define the vertices and create a new instance of BasicEffect and set some of the properties.Add the following lines of code in your Game class LoadContent method:

tmpD-77_thumb

In the previous code, you define three vertices of type VertexPositionColor.This vertex type holds a position and color for each vertex.You define the position and color for the top, right, and left vertices. The order you specify these is important because order is not culled by default in XNA Game Studio.

After you create your vertices, you create an instance of BasicEffect.The World, View, and Projection properties are used in the vertex shader portion of the graphics pipeline to transform the input vertices to draw on the screen. For now, don’t worry about the specific values for these matrices.The VertexColorEnabled property is set to true to tell the BasicEffect to use the color from each vertex as the output value of the pixel shader.

The final step is to issue the draw call using the DrawUserPrimitives method.Add the following lines of code to your Game class Draw method:

tmpD-78_thumb

Before you can call DrawUserPrimitives, tell the GraphicsDevice to use the BasicEffect.This is done by calling Apply on the EffectPass you use.

Next, call DrawUserPrimitives.This method is generic and expects the vertex type as the generic T type parameter. In this case, pass VertexPositionColor as the vertex type. The first parameter defines what PrimitiveType you want to draw. Specify TriangleList to draw a list of triangles.The second parameter is an array of vertices of the same type used for the generic T parameter. Pass the userPrimitives array you created previously. The third parameter is used to specify an offset into the array of vertices. If your array contains multiple primitive vertices and you want to draw only a subset of those, you can specify an offset into the source array. For your purpose, you don’t want any offset, so set the value to 0.The final parameter DrawUserPrimitives takes the total number of primitives to draw. If your array defines multiple primitives, specify that number to draw with this parameter. For this example, you draw only a single triangle, so specify a value of 1.

If you build and run the previous example, you should see a single triangle like that in Figure 4.22. Notice that each vertex has its own color. and the color of the middle sections of the triangle change across the triangle to each vertex.This is called interpolation and occurs on values that are specified per vertex. Because the color is specified per vertex, the different vertex colors have to interpolate over the pixels between the two vertices.

Now let’s update the example to draw multiple primitives using the LineStrip primitive. First, update where the vertices are created with the following lines of code:

tmpD-79_thumb

 

 

tmpD-80_thumb

 

 

Using DrawUserPrimitives to draw a single triangle

Figure 4.22 Using DrawUserPrimitives to draw a single triangle

Note

The lines are set to Color.White to help make them more visible. You can use any color just like you would for triangles.

Then, update your DrawUserPrimitives call to specify the LineStrip primitive type and the new number of primitives:

tmpD-82_thumb

If you run the example now, it should look like Figure 4.23.

Using DrawUserPrimitives to draw a LineStrip DrawUserIndexedPrimitives

Figure 4.23 Using DrawUserPrimitives to draw a LineStrip DrawUserIndexedPrimitives

If your primitives commonly use the same vertices for a number of different primitives, it can save you memory to use indexed primitives instead. To draw a user indexed primitive, you need to specify an array of indexes to use when drawing the triangles.These index values are used to look up each specific vertex to use when rendering the primitive.

To draw a user indexed primitive, update the current example.Add an array of index value. These are integer values that represent offsets into the vertex array you also pass into the DrawUserIndexedPrimitives method. Update the member variables for your Game class to have the additional index array like the following code:

tmpD-84_thumb

Use a 16-bit short array to store the index values. Because each index is represented by a short, the index values can be between only 0 and 65535, which is the maximum positive number a short can represent.You can also use an array of 32-bit int values, which can store positive values of more than four billion but this has two repercussions. The first is that storing index values as int values means that each index takes up two more bytes than when using shorts thus doubling the memory usage for your index val-ues.The other is that 32-bit int indexes are supported only in the HiDef GraphicsProfile, so your game must usea HiDef GraphicsDevice.

Next, create the vertices and index values for your primitives. In this case, draw a square, often called a quad, that is made of two triangles. Update the existing example’s LoadContent method to contain the following code:

tmpD-85_thumb

Like the previous example, create an array of VertexPositionColor to represent the vertices in your primitives.The code creates four vertices: one for each corner of the quad. A short array is then created to specify which vertex to use for each primitive. The first triangle uses the top left, top right, and bottom left vertices making sure to define them in clockwise order. The second triangle uses the top right, bottom right, and bottom left.

Finally, update your Game class Draw method to use the DrawUserIndexPrimitives method:

tmpD-86_thumb

Like the previous example with DrawUserPrimitives, the DrawUserIndexedPrimitives call is generic and you pass the VertexPositionColor as the generic T type parameter. The first parameter is again the primitive type. The second parameter is again the array of vertices.The third parameter is the vertex offset that should be used. For index primitives, this offset is added to each index value to obtain the final index to use in the vertex array. The fourth parameter is the number of vertices that is used in the draw call. In this case, use four vertices.This parameter is often the size of the vertex array itself unless you use offsets.The fifth parameter is the array used to store the index values. For the example, that is the userPrimitivesIndices variable.The sixth parameter is the offset into the index array that should be used. In this case, you don’t need to offset, so the value is 0.The final parameter is again the number of primitives to draw. For the example, you try two triangles, so a value of 2 is specified.

If you build and run the example code, you should see an image like that in Figure 4.24 where a four-color quad is draw on the screen.

Using DrawUserIndexedPrimitives to draw a quad

Figure 4.24 Using DrawUserIndexedPrimitives to draw a quad

Let’s lower the memory used by the index array by updating the example to just use four index values and use the TriangleStrip PrimitiveType. Update where you create the index array to look like the following:

tmpD-88_thumb

The second triangle is formed by using the last two index values plus the new additional value. In this case, the triangle is formed by userPrimitivesIndices[3] , userPrimitivesIndices[2] , and userPrimitivesIndices[1] .This second triangle saves 32 bits of data by not using two additional index values.

The DrawUserIndexedPrimitives method call needs to be updated to use only PrimitiveType.TriangleStrip and not TriangleList. If you run the example code now, you will not see any visible difference yet you are saving valuable memory space. It is not always possible to save space and use triangle strips; in these cases, you should use TriangleLists.

Next post:

Previous post: