此页面有用吗?
您对此内容的反馈非常重要。 请告诉我们您的想法。
更多反馈?
1500 个剩余字符
导出 (0) 打印
全部展开
EN
此内容没有您的语言版本,但有英语版本。

Into the Third Dimension

Applies to: Windows Phone 7

Published: August 2011

Author: Rod Stephens

WROX Tech Editors for Windows Phone 7 Articles

Wrox Windows Phone 7 Books

This topic contains the following sections.

Summary: This is the first in a series of articles that shows how to write a simple three-dimensional game for Windows Phone 7. The final Marvelous Maze game lets the user explore a simple maze looking for treasures and overcoming obstacles to reach a final goal. This article explains how to create objects in three-dimensional space. It explains how to transform those objects and use projections to display them in a Windows Phone 7 game. It explains a simple 3-D example and how culling lets XNA draw closed solids more efficiently.

This article is intended for .NET developers who are familiar with the C# programming language and the basics of Windows Phone 7 application development. It assumes you have the necessary hardware to run the Windows Phone 7 emulator, that you have installed Visual Studio 2010 Express for Windows Phone (or some more powerful version), and that you have installed Microsoft XNA Game Studio 4.0.

This article also assumes you have basic knowledge of displaying two-dimensional objects in Windows Phone. You can still read the articles and learn about the three-dimensional aspects of that information without knowing how to display two-dimensional objects but a few of the display features will not make as much sense. For an introduction to displaying two-dimensional objects in Windows Phone, see “How to: Create Your First XNA Framework Application for Windows Phone” at http://msdn.microsoft.com/library/ff472340.aspx.

Before you start reading this article, you should understand the basics of loading and displaying a two-dimensional image in Windows Phone. This article explains how to take the next step by moving into the third dimension. It explains how to perform the following steps:

  • Define objects in 3-D space.

  • Transform the objects by translating, rotating, and scaling.

  • Define a viewing matrix to orient the objects for viewing.

  • Define a projection matrix to convert the 3-D coordinates into 2-D screen coordinates.

  • Use effects to determine how the objects are colored.

  • Draw the objects.

This might seem like a lot of steps, but they are all necessary for drawing 3-D objects. To make matters worse, to really understand the drawing process, you need to understand all of these topics, at least to some extent. That makes it hard to present each topic in isolation.

The following sections describe these topics and provide code snippets to give you some idea of how they work, but you might not see the full picture until you study the complete example shown later in this article, so grab your favorite caffeinated beverage and try to be patient.

Windows Phone devices include powerful graphics processors that do most of the work of drawing three-dimensional objects quickly and automatically, but you still need to tell the processor what to draw. In Windows Phone 7 XNA programs, you determine what to draw by defining a group of triangles. If you really want to draw something else, such as a square, cube, or spaceship, you need to break it into triangles. If you use enough triangles and shade them properly, you can make them look like smoothly curved objects such as spheres and spaceships.

To describe a triangle, you need to describe its vertices. In XNA, you specify a vertex’s location by giving its X, Y, and Z coordinates in three-dimensional space. The X-axis extends to the right, the Y-axis extends upward, and the Z-axis extends out of the phone’s screen as shown in Figure 1.

Figure 1. The coordinate system used by XNA.

Referenced Image

This is called a right-handed coordinate system because you can use your right hand to help remember the directions that the axes point. Point your right thumb to the right (along the X-axis) and your index finger upward (along the Y-axis). Your palm and other fingers should now point toward your face (along the Z-axis). (Some graphics systems use a left-handed coordinate system, and you can perform a similar exercise with your left hand to discover that the Z-axis points away from you in those systems. For XNA phone game programming, however, stick to the right-handed system.)

In addition to specifying a vertex’s location, you have several ways to specify its color. For example, you can indicate that a corner of a triangle should be plain old red, a shade of red that depends on how a light source falls on it, or a color taken from a bitmap, such as a brickwork or wood grain texture. I will discuss some of the more interesting color techniques in later articles. For now, I will focus on specifying vertex colors explicitly.

The VertexPositionColor class, which is defined by the XNA framework, stores information about a vertex’s location and color. The class has Position and Color properties that you can set, but often it is easier to use the class’s constructor to initialize the object.

To define a 3-D object, you create an array of VertexPositionColor objects defining where the object’s vertices are. For example, the following code defines an array holding three VertexPositionColor objects that define a single triangle.

VertexPositionColor[] triangleVertices;

...

// Make the triangle.
triangleVertices = new VertexPositionColor[3];
i = 0;
triangleVertices[i++] = new VertexPositionColor(new Vector3(0, 8, 0), Color.Green);
triangleVertices[i++] = new VertexPositionColor(new Vector3(8, 0, 0), Color.Red);
triangleVertices[i++] = new VertexPositionColor(new Vector3(0, 0, 8), Color.Blue);

This code declares the triangleVertices array. Later it allocates the array and fills it with three VertexPositionColor objects representing the points (0, 8, 0), (8, 0, 0), and (0, 0, 8), making these points green, red, and blue, respectively.

At this point, the program is almost ready to draw, but before you can draw the points, you need to learn a bit about transformations and cameras.

In 3-D graphics programming, a transformation is an operation that modifies the vertices in a 3-D model. In theory, transformations can twist and warp the vertices in arbitrary ways, but in practice, the three most common transformations are translation, scaling, and rotation.

  • Translation moves a vertex. For example, you could add 10 to a vertex’s X coordinate, 5 to its Y coordinate, and -10 to its Z coordinate.

  • Scaling multiplies a vertex’s coordinates by some values. For example, you could multiply all of a vertex’s coordinates by 2. If the vertices represent an object, this will move the object twice as far from the origin as its original position. It will also make the object twice as long, wide, and tall as it was before.

  • Rotation rotates a vertex around an axis. Often the axis of rotation is one of the coordinate axes (for example, the Y-axis), but it could be any arbitrary line.

Another common transformation, reflection, reflects a vertex across a plane. (Think of the plane as a mirror.)

You can make complex transformations by combining simpler ones. For example, suppose a cube is centered at the point (10, 10, 10). If you scale it by a factor of 2, its new center is at (20, 20, 20). But suppose you wanted only to enlarge the cube and not move it. You can do that by first translating the cube so that it is centered at the origin, then scaling it, and finally translating it back to its original position.

An XNA program can represent one of these transformations with an instance of the Matrix class. This class represents a mathematical 4 x 4 matrix of numbers. If you multiply a point represented as a vector by a transformation matrix, the result is a new vector representing the transformed point. For example, if a matrix represents rotation around the Y axis by 45 degrees, then if you multiply a point by this matrix, the result is the point appropriately rotated. The mathematics behind 3-D transformations is elegant and fascinating, but it is also a bit outside the scope of these articles. For more information, see a book about three-dimensional computer graphics or search online. For example, you can find Wikipedia’s “Transformation matrix” entry at http://en.wikipedia.org/wiki/Transformation_matrix.

The Matrix class provides a constructor that lets you initialize these numbers directly, but it also provides several static methods for creating transformation matrices, and those methods are much more intuitive. For example, the CreateTranslation method returns a Matrix object representing a translation. The following list summarizes the Matrix class’s basic transformation creation methods:

  • CreateFromAxisAngle—Rotates around an arbitrary line.

  • CreateFromYawPitchRoll—Rotates using yaw, pitch, and roll parameters.

  • CreateReflection—Reflects across a plane.

  • CreateRotationX—Rotates around the X-axis.

  • CreateRotationY—Rotates around the Y-axis.

  • CreateRotationZ—Rotates around the Z-axis.

  • CreateScale—Scales.

  • CreateTranslation—Translates.

One of the nice things about this kind of matrix is that you can represent a complex series of transformations by multiplying their matrices together. For example, consider the following code:

Matrix translate1 = Matrix.CreateTranslation(-10, -10, -10);
Matrix scale = Matrix.CreateScale(2);
Matrix translate2 = Matrix.CreateTranslation(10, 10, 10);
Matrix transformation = translate1 * scale * translate2;

This code creates a Matrix object representing a translation by -10 along the X-axis, -10 units along the Y-axis, and -10 units along the Z-axis. Second, it makes a scaling Matrix object that scales in the X, Y, and Z directions by a factor of 2. Third, the code creates a translation Matrix object that reverses the first transformation.

Finally, the code creates a combined transformation Matrix object that applies the first translation, followed by the scaling matrix, followed by the second translation. This final Matrix object is equivalent to the other three. In other words, you could transform a vertex by translating it, scaling it, and then translating it a second time. Alternatively, you could just transform it once using the combined transformation. The result is the same, but for a long series of transformations, you can save a lot of work by applying only a single transformation matrix.

Note that the order in which you apply transformations is important. In general, a rotation followed by a translation is not the same as the translation followed by the rotation.

Using transformations, you can manipulate the objects in your 3-D environment. For example, suppose your game has a monster wandering around. Rather than trying to figure out how to draw the monster in various positions, you can create points to draw it centered at the origin and then use a transformation to move it to its proper location. Similarly, you can rotate the monster so that it faces in different directions. You could even scale it to make it smaller if the player hits it with a shrink ray.

After reading the previous two sections, you can (at least in principle) create 3-D vertexes and transform them if you want. At this point, you can think of your data as living in an imaginary 3-D world, sometimes called world coordinate space. There are two more major steps you need to take before you can display that world on a phone or the phone emulator: creating a viewing transformation and creating a projection.

The viewing transformation orients the objects so that they are arranged in the way that you want them displayed. You can think of this as positioning a camera inside the imaginary world and pointing it in a certain direction. You can also roll or tilt the camera to the side to make everything tilted.

Defining a viewing transformation directly could be tricky, but fortunately, the Matrix class provides a handy CreateLookAt method that does the job in a fairly intuitive way. This method takes three parameters: the camera’s position, the target location toward which the camera is pointed, and a vector indicating the camera’s “up” direction. The last parameter, the “up” direction, controls the camera’s tilt.

Figure 2 shows the idea graphically. To understand the effect of the “up” vector, imagine rolling the camera around the dashed arrow pointing toward the target.

Figure 2. The viewing transformation represents the camera’s position, direction, and tilt.

Referenced Image

The following code makes a viewing transformation.

Vector3 cameraPosition = new Vector3(0, 20, 50);
Vector3 cameraTarget = new Vector3(0, 0, 0);
Vector3 upVector = new Vector3(0, 1, 0);
Matrix viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraTarget, upVector);

The first line of code makes a Vector3 object representing the camera’s position. A Vector3 object represents a direction, but you can also think of it as representing the point you get if you start at the origin and move along the vector.

The second line defines the camera’s target. This is the point toward which the camera is pointed.

The third line creates the “up” vector. This vector does not need to be exactly perpendicular to the arrow pointing from the camera towards the target; it just needs to give the basic orientation of the camera.

Quite often in 3-D programs, the target is at the origin (0, 0, 0), the “up” vector points along the Y-axis, and the camera’s position moves around to look at the origin from different directions.

The final logical step in displaying a 3-D scene is projecting the data from three dimensions into two. After all, your phone only has a two-dimensional screen (at least today’s phones do). At some point, you need to convert your objects’ three-dimensional coordinates to work on the two-dimensional screen.

You can use two basic kinds of projection: orthographic and perspective. An orthographic projection simply drops the vertices’ Z coordinates. Imagine taking a model made out of pipe cleaners, orienting it so that the camera sits above it looking down, and then squashing the model onto the floor. The result is an orthographic projection. In an orthographic projection, parallel line segments retain their relative lengths so that an object that is farther away from the camera has the same size as a similar object closer to the camera.

In contrast, a perspective transformation makes objects that are farther away appear smaller than those that are closer to the camera. This is the way your eye works, so it often gives a more realistic result than an orthographic transformation. Despite the greater realism, some early video games used orthographic projections because they are easier to calculate, and early video game machines had much less computing power than is provided by modern computers (and phones).

Figure 3 shows the orthographic and perspective projections of a cube. In the perspective projection, I have placed the camera fairly close to the cube and given it a wide field of view, so the perspective is somewhat exaggerated.

Figure 3. Orthographic (left) and perspective (right) projections of a cube.

Referenced Image

With a little work, you could create the transformation matrix yourself. Fortunately the Matrix class makes this easier by once again providing static methods for producing these transformations for you.

As you can probably guess, the CreateOrthographic and CreateOrthographicOffCenter methods create orthographic transformation matrices. Similarly, the CreatePerspective, CreatePerspectiveFieldOfView, and CreatePerspectiveOffCenter methods create perspective transformation matrices.

The following code snippet creates a perspective transformation matrix.

projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
    MathHelper.PiOver2, device.Viewport.AspectRatio, 1.0f, 300.0f);

The parameters passed to the CreatePerspectiveFieldOfView method are as follows

  • Field of view—The angular field of view in the Y direction in radians. This value must be between 0 and π, non-inclusive. Large values make the projection include a wide area, similar to the effect of a fisheye lens.

  • Aspect ratio—Determines the aspect ratio (ratio of width to height) of the result. If you do not want to distort the result, set this to the same aspect ratio as the viewport (screen), as shown in this example.

  • Near plane distance—Sets the minimum distance from the camera that an object must be to remain visible. Objects closer to the camera than this distance are removed.

  • Far plane distance—Sets the maximum distance from the camera that an object can be and still be visible. Objects farther from the camera than this distance are removed.

The final appearance of a 3-D object depends on several factors, including the locations, colors, and types of lights shining on it. For example, a sphere looks different under a weak blue light than under a bright white spotlight. To specify these and other drawing parameters to the graphics hardware, you use an effect. An effect contains one or more techniques that specify different drawing parameters.

You can think of techniques as drawing styles. For example, one technique might specify a bright white light shining on objects while another specifies a dim blue light.

Each technique can include multiple passes. More elaborate drawing techniques might require a program to use multiple passes to render an object. For example, you might use multiple passes to represent multiple light sources, you could use one pass to draw an object in its normal solid appearance and then use a second pass to draw a wireframe on top, or you could use multiple passes to create horizontal and vertical motion blur. Simple programs often use only a single effect that defines one technique holding a single pass, so you probably do not need to worry about multiple passes for quite a while.

You can make a single effect object include many techniques and then select the ones you want to use when you draw. Each technique can be fairly complex, so the end result might be quite involved.

Building an Effect object gives you great control over the various shaders that contribute to the object’s color, but it is also somewhat confusing. Fortunately, you can get a lot of mileage out of the BasicEffect class. This class includes a technique that defines a relatively simple but useful lighting model. To use a BasicEffect object, your program should do the following:

  • Create the BasicEffect object.

  • Set properties to indicate the type of lighting model you want to use. For example, to display vertices that have color information, the program would set the effect’s VertexColorEnabled property to true.

  • Set the effect’s World, View, and Projection transformation matrices.

  • Draw.

The next section explains how the program actually draws something and, at long last, describes a working example. In that example’s code, you’ll see how the program creates, initializes, and uses the BasicEffect object.

After all of this preliminary work, you are finally ready to draw some objects.

The game’s GraphicsDevice object provides methods for working with the phone’s screen. Its Viewport property returns an object that has X, Y, Width, Height, and Aspect properties to give you information about the screen’s size.

The GraphicsDevice object also provides a DrawUserPrimitives method that lets you draw 3-D objects. The following code shows the DrawUserPrimitives call used by the ColoredTriangle example program.

// Draw the triangles.
GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList,
    triangleVertices, 0, 1, VertexPositionColor.VertexDeclaration);

The first parameter passed to DrawUserPrimitive tells the graphics hardware what type of objects to draw. This can be TriangleList (the vertices define a series of triangles, three vertices per triangle), TriangleStrip (the first three vertices define a triangle and each subsequent vertex defines a new triangle with the two previous points), LineList (the vertices define a series of line segments, two vertices per segment), or LineStrip (draws a series of segments connecting the vertices).

The next two parameters give the array of vertices and the index in the array at which to start drawing.

The fourth parameter gives the number of objects to draw. This number is the number of triangles or lines, not the number of vertices needed to draw them. For example, a triangle list would require 30 points to represent 10 triangles. In that case, this parameter should be 10.

The final parameter tells the graphics hardware what kind of information is in the vertex array. In this example, the vertices store position and color information.

The following code shows the DrawTriangle method that the example uses to call DrawUserPrimitive. The game loop’s Draw method calls DrawTriangle to draw on the phone.

// Draw the colored triangle.                           // Note 15
private void DrawTriangle()
{
    // Set the projection and view matrices.
    triangleEffect.World = worldMatrix;
    triangleEffect.View = viewMatrix;
    triangleEffect.Projection = projectionMatrix;

    // Process each Technique in the Effect.
    foreach (EffectPass pass in triangleEffect.CurrentTechnique.Passes)
    {
        // Apply the technique.
        pass.Apply();

        // Draw the triangles.
        GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList,
            triangleVertices, 0, 1, VertexPositionColor.VertexDeclaration);
    }
}

Code earlier in the program creates the triangleEffect object. (You will see that code in the complete example shortly.) The DrawTriangle method sets the effect’s world, view, and projection transformations. In this example, the view transformation rotates the triangle over time, the projection transformation places the “camera” at (0, 20, 50) looking at the origin, and the projection is a perspective transformation.

Next the code loops through the Passes collection of the effect object’s CurrentTechnique property. BasicEffect objects can have only one pass, so you could skip the loop and just use this code.

triangleEffect.CurrentTechnique.Passes[0].Apply();

Many developers use the loop anyway, in case triangleEffect is later changed to a more general effect object.

Within the foreach loop, the code calls the current pass’s Apply method. It then calls the GraphicsDevice object’s DrawUserPrimitive method to render the object. This example draws a triangle list containing one triangle, using the vertex position and color information.

All of this information may seem a bit shaky because none of it can stand completely by itself. This is similar to the way you cannot build the left half of a three-story house of cards. All of the pieces need to fit together, or it will not stand.

To see how it all works, consider the ColoredTriangle example program shown in Figure 4.

Figure 4. The ColoredTriangle program draws a single colored triangle in 3-D space.

Referenced Image

This program might not seem like a big deal, but it demonstrates all of the basic methods you need to draw simple 3-D scenes. (You also cannot tell from the figure, but the triangle is rotating in 3-D space.)

The following code shows the example’s complete source code. The most interesting pieces of code include a comment of the form “Note 7.” A list of notes after the code explains the corresponding pieces of code.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;

namespace ColoredTriangle
{
    /// <summary>
    /// This is the main type for your game.
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        //SpriteBatch spriteBatch;                          // Note 1

        // Projection geometry.                             // Note 2
        Vector3 cameraPosition = new Vector3(0, 20, 50);
        Vector3 cameraTarget = new Vector3(0, 0, 0);
        Vector3 upVector = new Vector3(0, 1, 0);

        // Viewing and transformation matrices.
        Matrix worldMatrix, viewMatrix, projectionMatrix;   // Note 3
        public float angle = 0f;                            // Note 4

        // 3-D objects.                                     // Note 5
        VertexPositionColor[] triangleVertices;
        BasicEffect triangleEffect;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            // Frame rate is 30 fps by default for Windows Phone.
            TargetElapsedTime = TimeSpan.FromTicks(333333);
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here.

            // Required or the game does not rotate.         // Note 6
            graphics.SupportedOrientations =
                DisplayOrientation.LandscapeLeft |
                DisplayOrientation.LandscapeRight |
                DisplayOrientation.Portrait;

            // Make the preferred back buffer fit the current viewport.
            graphics.PreferredBackBufferWidth =
                GraphicsDevice.Viewport.Width;
            graphics.PreferredBackBufferHeight =
                GraphicsDevice.Viewport.Height;

            // Hog the whole screen.
            graphics.IsFullScreen = true;

            graphics.ApplyChanges();
            Window.Title = "ColoredTriangle";

            // Catch the OrientationChanged event.          // Note 7
            this.Window.OrientationChanged += OrientationChanged;

            base.Initialize();
        }

        // The user rotated the phone.                      // Note 8
        // Reset the projection matrix to match the aspect ratio.
        private void OrientationChanged(object sender, EventArgs e)
        {
            SetProjection();
        }

        // Set an appropriate projection for this orientation.  // Note 9
        private void SetProjection()
        {
            if (this.Window.CurrentOrientation == DisplayOrientation.Portrait)
            {
                projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
                    MathHelper.PiOver4, GraphicsDevice.Viewport.AspectRatio, 1.0f, 300.0f);
            }
            else
            {
                projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
                    MathHelper.Pi / 6, GraphicsDevice.Viewport.AspectRatio, 1.0f, 300.0f);
            }
        }
        
        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            //spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here.


            // Make 3-D objects.                                // Note 10

            // Make the triangle.
            triangleVertices = new VertexPositionColor[3];
            int i = 0;
            triangleVertices[i++] = new VertexPositionColor(new Vector3(0, 13, 0), Color.Green);
            triangleVertices[i++] = new VertexPositionColor(new Vector3(13, -13, 0), Color.Red);
            triangleVertices[i++] = new VertexPositionColor(new Vector3(-13, -13, 0),
                Color.Blue);

            // Create the Effect and indicate                   // Note 11
            // we are using vertices with color.
            triangleEffect = new BasicEffect(GraphicsDevice);
            triangleEffect.VertexColorEnabled = true;
            
            // Define the camera and projection.
            SetUpCamera();
            SetProjection();
        }

        // Define the camera.                                   // Note 12
        private void SetUpCamera()
        {
            viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraTarget, upVector);
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit.
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // TODO: Add your update logic here.


            // Update the angle of rotation.                    // Note 13
            // 4 seconds per rotation or 2 * pi / 4 radians per second.
            angle += (float)(MathHelper.TwoPi / 4 * gameTime.ElapsedGameTime.TotalSeconds);


            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here.

            
            // Rotate around the Y axis.                        // Note 14
            worldMatrix = Matrix.CreateRotationY(angle);

            // Draw the colored triangle.
            DrawTriangle();


            base.Draw(gameTime);
        }

        // Draw the colored triangle.                           // Note 15
        private void DrawTriangle()
        {
            // Set the projection and view matrices.
            triangleEffect.World = worldMatrix;
            triangleEffect.View = viewMatrix;
            triangleEffect.Projection = projectionMatrix;

            // Process each Technique in the Effect.
            foreach (EffectPass pass in triangleEffect.CurrentTechnique.Passes)
            {
                // Apply the technique.
                pass.Apply();

                // Draw the triangles.
                GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, triangleVertices,
                    0, 1, VertexPositionColor.VertexDeclaration);
            }
        }
    }
}

The following notes explain the most interesting pieces of the code

  1. This program does not draw any two-dimensional bitmaps, so it does not need a SpriteBatch object. I have commented it out here but left it in to emphasize the fact that Visual Studio’s Windows Phone Game template creates a SpriteBatch object for you. You can delete it from your program if you wish.

    //SpriteBatch spriteBatch;                          // Note 1
    
  2. This code declares some Vector3 objects that help the code define the viewing transformation. You could put these parameters directly into the code that creates the viewing matrix, but placing them here makes it easier to change.

    // Projection geometry.                             // Note 2
    Vector3 cameraPosition = new Vector3(0, 20, 50);
    Vector3 cameraTarget = new Vector3(0, 0, 0);
    Vector3 upVector = new Vector3(0, 1, 0);
    
  3. This line declares the matrices that define the world, view, and projection matrices. They are initialized and used later.

    // Viewing and transformation matrices.
    Matrix worldMatrix, viewMatrix, projectionMatrix;   // Note 3
    
  4. This line defines a variable that represents the triangle’s angle of rotation. The rendering loop’s Update method updates this, and its Draw method uses it to draw the triangle rotated at the appropriate angle.

    public float angle = 0f;                            // Note 4
    
  5. The triangleVertices variable is the array that holds the triangle’s vertex information. The triangleEffect variable holds the BasicEffect object the program uses to draw.

    // 3-D objects.                                     // Note 5
    VertexPositionColor[] triangleVertices;
    BasicEffect triangleEffect;
    
  6. These lines of code indicate that this game can display in either portrait or landscape orientation. It sets the graphics object’s preferred buffer size to fit the current screen orientation and indicates that the program will run in full-screen mode. It also sets the window’s title.

    // Required or the game does not rotate.         // Note 6
    graphics.SupportedOrientations =
        DisplayOrientation.LandscapeLeft |
        DisplayOrientation.LandscapeRight |
        DisplayOrientation.Portrait;
    
    // Make the preferred back buffer fit the current viewport.
    graphics.PreferredBackBufferWidth =
        GraphicsDevice.Viewport.Width;
    graphics.PreferredBackBufferHeight =
        GraphicsDevice.Viewport.Height;
    
    // Hog the whole screen.
    graphics.IsFullScreen = true;
    
    graphics.ApplyChanges();
    Window.Title = "ColoredTriangle";
    
  7. This statement registers the OrientationChanged event handler so that the program can make adjustments if the user changes the phone’s orientation while the program is running.

    // Catch the OrientationChanged event.          // Note 7
    this.Window.OrientationChanged += OrientationChanged;
    
  8. This is the OrientationChanged event handler. It calls the SetProjection method, described next, to create the appropriate projection matrix for the phone’s current orientation.

    // The user rotated the phone.                      // Note 8
    // Reset the projection matrix to match the aspect ratio.
    private void OrientationChanged(object sender, EventArgs e)
    {
        SetProjection();
    }
    
  9. The SetProjection method checks the phone’s orientation. If the phone is in the Portrait orientation, the code creates a reasonable projection matrix. If the phone is in one of the landscape orientations, the code uses a slightly narrower field of view to make the image a bit larger. The program still works if you use the previous projection in any orientation, but the objects appear slightly smaller in the landscape orientations. Try using the PiOver4 field of view for all orientations and see for yourself. The parameters used here produce reasonably nice results in all three orientations, and you might be able to use them in many programs.

    // Set an appropriate projection for this orientation.  // Note 9
    private void SetProjection()
    {
        if (this.Window.CurrentOrientation == DisplayOrientation.Portrait)
        {
            projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
            MathHelper.PiOver4, GraphicsDevice.Viewport.AspectRatio, 1.0f, 300.0f);
        }
        else
        {
            projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
                MathHelper.Pi / 6, GraphicsDevice.Viewport.AspectRatio, 1.0f, 300.0f);
        }
    }
    
  10. This is the code that creates the three-dimensional vertex data. This example draws only a single triangle with three vertices, so it is easy to include all of the code here. When you draw more complicated scenes, you might want to move the object creation code into separate methods and invoke them here to keep the code less cluttered. This code allocates the vertex array and fills it with vertices.

    // Make 3-D objects.                                // Note 10
    
    // Make the triangle.
    triangleVertices = new VertexPositionColor[3];
    int i = 0;
    triangleVertices[i++] = new VertexPositionColor(new Vector3(0, 13, 0), Color.Green);
    triangleVertices[i++] = new VertexPositionColor(new Vector3(13, -13, 0), Color.Red);
    triangleVertices[i++] = new VertexPositionColor(new Vector3(-13, -13, 0),
    Color.Blue);
    
  11. The code then defines the BasicEffect object and sets the BasicEffect object’s VertexColorEnabled property to true. This is required if the vertex data contains color information, as it does in this example. If you do not set this property, the triangle is white. Finally this piece of code calls the SetUpCamera and SetProjection methods to define the viewing and projection matrices.

    // Create the Effect and indicate                   // Note 11
    // we are using vertices with color.
    triangleEffect = new BasicEffect(GraphicsDevice);
    triangleEffect.VertexColorEnabled = true;
    
    // Define the camera and projection.
    SetUpCamera();
    SetProjection();
    
  12. The SetUpCamera method defines the viewing transformation using the cameraPosition, cameraTarget, and upVector values defined earlier. (See Note 2.)

    // Define the camera.                                   // Note 12
    private void SetUpCamera()
    {
        viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraTarget, upVector);
    }
    
  13. Inside the render loop’s Update method, the program increases the angle variable to rotate the triangle. Notice that the code multiplies TwoPi / 4 by the time that has elapsed since the last call to Update. That makes angle increase by TwoPi / 4 radian (1/4 of a complete rotation) per second.

    // Update the angle of rotation.                    // Note 13
    // 4 seconds per rotation or 2 * pi / 4 radians per second.
    angle += (float)(MathHelper.TwoPi / 4 * gameTime.ElapsedGameTime.TotalSeconds);
    
  14. The render loop’s Draw method uses the angle variable to create the world transformation matrix and then calls the DrawTriangle method.

    // Rotate around the Y axis.                        // Note 14
    worldMatrix = Matrix.CreateRotationY(angle);
    
    // Draw the colored triangle.
    DrawTriangle();
    
  15. The Draw method sets the BasicEffect object’s world, view, and projection transformations to the matrices created by other pieces of code. When it renders objects, the drawing system first transforms the objects by using the world transformation (in this case, rotating the objects), then applies the viewing transformation to orient the objects, and finally applies the projection to produce the final result.After preparing the BasicEffect object, the code loops through the current technique’s passes (although there will be only one for a BasicEffect object). For each pass, it applies the pass and calls DrawUserPrimitives.

    // Draw the colored triangle.                           // Note 15
    private void DrawTriangle()
    {
        // Set the projection and view matrices.
        triangleEffect.World = worldMatrix;
        triangleEffect.View = viewMatrix;
        triangleEffect.Projection = projectionMatrix;
    
        // Process each Technique in the Effect.
        foreach (EffectPass pass in triangleEffect.CurrentTechnique.Passes)
        {
            // Apply the technique.
            pass.Apply();
    
            // Draw the triangles.
            GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, triangleVertices,
                0, 1, VertexPositionColor.VertexDeclaration);
        }
    }
    

Now that you understand how the code works, you might want to read the whole thing again, keeping in mind the functions of each of the major methods. LoadContent loads the graphical content. The render loop’s Update method updates objects, in this case by increasing the angle variable. Draw uses the angle variable to create a world transformation that rotates the objects, and then draws them.

If you run the ColoredTriangle example program, you might be surprised to see the triangle disappear every now and then. The triangle looks like a flat triangle on one side but is invisible on the other. The reason it does this is because, by default, XNA programs use culling or backface removal.

To understand what culling is, consider the Tetrahedrons example program shown in Figure 5. The program draws two intersecting tetrahedrons. Parts of each tetrahedron are hidden because they lie inside the other. Figuring out exactly which parts of the tetrahedrons are visible is tricky. Fortunately, XNA and the graphics hardware figures that out for you so that you do not need to do it yourself.

Figure 5. XNA programs use culling to remove faces that are guaranteed to be hidden.

Referenced Image

While the general hidden surface problem is difficult, some surfaces that are easy to identify should be hidden, such as surfaces on the far side of the tetrahedrons. For example, the upper tetrahedron has sides that are blue, red, orange (on the back side), and light green (on the bottom). The orange and light green sides are on the far side of that tetrahedron as seen from the camera’s position, so they are completely hidden.

If the graphics system can recognize these surfaces, it can quickly exclude them from further consideration and spend more time on the more complex situations, such as one object sitting in front of another or, as in this example, one triangle piercing another. Removing these back surfaces is called culling or backface removal.

To make culling work, you need to make the 3-D scene out of outwardly-oriented triangles. In an outwardly-oriented triangle, the points that make up the triangle are arranged clockwise when you view the triangle from the outside of the solid.

For example, look at the large red triangle shown in Figure 5. (Remember that all four red pieces shown are part of one big red triangle that is partially obscured by the bottom tetrahedron.) You could place the points in the vertex array in the order lower left, top, lower right, as seen in the figure. That would be a clockwise orientation for the points.

Now imagine that you rotate the figure 180 degrees around the vertical Y-axis. The relative positions of the lower points have switched, so what was the lower-left corner is now on the right. Looking through the tetrahedron, the points are now oriented counter-clockwise.

The way culling works is that the graphics system prepares the triangles for projection and then looks at their points. If a triangle is oriented counter-clockwise, it is culled and discarded without any further consideration. If the 3-D scene mostly contains solid objects, this can remove about half of the triangles from consideration, greatly speeding up the rendering process.

Note that this works only on solid objects that are completely closed. If you have a hole in a sphere, for example, you might want the user to see the inside of the sphere through the hole. In cases such as this, and in the ColoredTriangle example, you can turn off culling by adding the following code snippet to the Draw method before drawing.

// Turn off culling.
RasterizerState rs = new RasterizerState();
rs.CullMode = CullMode.None;
GraphicsDevice.RasterizerState = rs;

This code simply creates a RasterizerState object, sets its CullMode property to None, and then sets the GraphcisDevice object’s RasterizerState property to the new object so that it knows to skip culling. For closed solids, you get the same result with or without culling but the program is a bit slower without culling. For non-closed shapes such as the ColoredTriangle program’s triangle, the user sees both sides of the triangle.

You can also set the CullMode property to CullCounterClockwiseFace, to cull triangles that are oriented counter-clockwise (the default behavior), or to CullClockwiseFace, to cull triangles that are oriented clockwise. To avoid confusion, you should normally stick to one orientation or the other and not mix the two.

When you are building a complicated program, it is common to mess up the orientation of one or two triangles. When you do, the result looks mostly normal, but some triangles might be missing and others might mysteriously appear and disappear unexpectedly as the scene moves or rotates. You might want to try changing the culling in the Tetrahedrons program to CullClockwiseFace so that you can see what this looks like in extreme cases. When you see this sort of effect, you should review your triangles to see whether any are all oriented incorrectly.

This article explains how to draw 3-D triangles on the screen. By drawing enough triangles, you can build scenes that are much more complex than the one shown in Figure 5. Unfortunately, more complex scenes are harder to build and debug than the simple ones shown here. If you build a scene containing hundreds or even thousands of triangles, it can take a long time to figure out which ones are incorrect if some are oriented counter-clockwise.

It is also difficult to create realistically colored results by using only colored triangles. For example, suppose you want to make a green cube. You would need to give each side a slightly different shade of green, or the user will be unable to tell where one side ends and another begins. The problem is even greater if you want to draw a smoothly shaded sphere or torus containing hundreds of triangles. Assigning colors to each of the triangles individually would be extremely tiresome. Creating surfaces that look like they are made of wood, brickwork, or other textures is practically impossible with only colored triangles.

The next article in this series explains lighting models that you can use to give objects a more realistic appearance. It also explains methods you can use to make more complicated objects while minimizing tedious programming and debugging. It shows how to create classes to represent objects such as boxes, spheres, and tori (donuts), and how to use simpler objects to build more complex ones.

Previous Article: Building 3D Games for Windows Phone

Continue on to the Next Article: Lighting

显示:
© 2015 Microsoft