NeHe Tutorial #2 - Drawing 3D Primitives

14. April 2010 12:07 by Jeromy Walsh in Tutorials  //  Tags: ,   //   Comments (13)

In Lesson 2 of this tutorial series we make our first attempt at working with the XNA 4.0 3D Graphics Pipeline. By the time this tutorial is complete you should have a working knowledge of how to get 3D primitives onto the screen, how to work with Effects to perform Real-Time shading on the GPU, and as usual, some of the differences between XNA 3.1 and XNA 4.0. So Let's get started....

In order to avoid duplicating large amounts of code each post, I'm only going to focus on the things that are different between the current project and that provided in NeHe Tutorial #1. Luckily, that happens right away. The first change to the project is the addition of several new fields. As you can see in the code below, I've included a VertexDeclaration, a couple of matrices which I'll use for world transforms, and a couple of vector arrays. The other new arrival is a member of type "DefaultEffect", which we'll be implementing ourselves.

public class Game1 : Microsoft.Xna.Framework.Game
{
    GraphicsDeviceManager graphics;
    KeyboardState prevKeyboardState = Keyboard.GetState();

    DefaultEffect effect;
    VertexDeclaration vertexDecl;
    Matrix triangleTransform;
    Matrix rectangleTransform;

    Vector3[] triangleData;
    Vector3[] rectangleData;

I should take a moment and explain a bit about DefaultEffect. Back in the late 90's and early 2000's, hardware accelerated T&L had become the defacto standard, however it all used the fixed-function pipeline which was present in DirectX 9 and below. Gradually, starting in the early 2000's, people began to slowly make the transition from fixed-function pipelines to programmable shaders. However, this was/is an extremely slow process. I suspect this was at first due to the esoteric nature of shaders. We had to implement them in GPU ASM, they had to be compiled offline and stored as byte arrays, and they were difficult (seemingly impossible) to debug. But times have changed....

We can now use HLSL, Cg, and GLSL to implement our shader programs, they can be debugged with tools such as PIX, and we can create .fx files in order to make bundling and maintaining shaders easier. So now what's the excuse?  My guess is the mathematics. Many people are still afraid of 3D transformations. The increased responsibility of having to perform the math ourselves can be a bit intimidating, and it scares a large number of programmers off. Even with the advent of XNA many people take the path of least resistance and avoid diverging from BasicEffect because it appears to behave like the Fixed-Function Pipeline, and allows the programmer to avoid doing any mathematics. But the truth is, BasicEffect doesn't use the fixed-function pipeline either, it never did. BasicEffect is just a pretty bow wrapped around a series of shaders which provides a simple, intuitive way to interact with a program which runs on the GPU. There's really nothing magical about it. So in an effort to help stop the mindless use of BasicEffect, I'll be working with ONLY custom effects in this series. No BasicEffect.

As a side note, this is a bit contrary because with XNA 4.0 Microsoft has included several new pre-packaged effects which cover some of the most desirable rendering operations. Specifically, XNA 4.0 includes:

  • AlphaTestEffect
  • BasicEffect
  • DualTextureEffect
  • EnvironmentMapEffect
  • SkinnedEffect

Each of these are just children of the Effect class which expose EffectParameters in the form of Properties, and occasionally perform some additional device setup. But they're now available and I encourage you to use them if you can, as it does save development time (but you should learn how to implement them on your own as well). And incidentally, if you'll be targeting the new WinPhone 7 device with your XNA games you better get comfortable with those Effect classes. WinPhone does NOT support custom effects. Only those provided by the XNA Framework.

At any rate, let's move on. The following code is just the filled in OnClientSizeChanged method we created in tutorial 1. This just calls ResetProjection which we use to establish the projection matrix used by our renderer. I chose to use the CreatePerspectiveFieldOfView method to do this, however you could use any of the projection methods on the Matrix class, or you could build the projection matrix yourself if you know how. Because this method is called from inside OnClientSizeChanged, every time we resize the window, the projection matrix will be re-computed so our 3D objects can re-draw themselves with the new aspect ratio.

protected void OnClientSizeChanged(object sender, EventArgs e)
{
    ResetProjection();
}

protected void ResetProjection()
{
    Viewport viewport = graphics.GraphicsDevice.Viewport;

    // Set the Projection Matrix
    effect.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,
        (float)viewport.Width / viewport.Height, 
        0.1f, 
        100.0f);
}

The next method I provide is the Initialize method. In this method I create two matrices which will act as the world transformations for a 3D triangle and rectangle on the screen, and then I provide the vertex positions for both.

protected override void Initialize()
{
    // Create the transforms for the two shapes we want to draw
    triangleTransform  = Matrix.CreateTranslation(new Vector3(-1.5f, 0.0f, -6.0f));
    rectangleTransform = Matrix.CreateTranslation(new Vector3(1.5f, 0.0f, -6.0f));            

    // Initialize the triangle's data
    triangleData = new Vector3[3];
    triangleData[0] = new Vector3(1.0f, -1.0f, 0.0f);
    triangleData[1] = new Vector3(-1.0f, -1.0f, 0.0f);
    triangleData[2] = new Vector3(0.0f, 1.0f, 0.0f);

    // Initialize the Rectangle's data
    rectangleData = new Vector3[4];
    rectangleData[0]= new Vector3(-1.0f, -1.0f, 0.0f);
    rectangleData[1]= new Vector3(-1.0f, 1.0f, 0.0f);
    rectangleData[2]= new Vector3(1.0f, -1.0f, 0.0f);
    rectangleData[3]= new Vector3(1.0f, 1.0f, 0.0f);

    base.Initialize();
}

One thing to note here is the z-value. XNA uses a Right-Handed coordinate system so positive-z is viewed as coming out of the screen. Setting the world transforms to have a -6 means I'm moving them 6 units into the screen (further in front of the camera). The nice thing about this is that it makes the primitives visible without having to move my camera at all, which defaults to being at (0,0,0) looking down the negative z-axis. This also means, for the purpose of this demo, I don't need to worry about a view transformation. As is, the view matrix is the identity matrix, and everything is already in view space. (view and world space are the same).

The next thing the method does is initialize a set of vertices for the triangle and rectangle. You probably noticed in the member fields that I'm not including any index or vertex buffers in this demo. I'll get to it in a tutorial or two, but for now I wanted to focus on one thing at a time. As a result, we're going to just use user-provided arrays to act as our vertex data. Because we only have a 3D position (no colors or tectures) the only thing we need to represent our vertices are Vector3's.

Having shown the Initialize method it's now time to show the LoadContent method. In here we create the DefaultEffect which was discussed previously. You'll notice that on the first three lines of the method block I load the effect from file using the ContentManager, pass it off to the DefaultEffect constructor in order to create an instance of my custom effect class, and then set the loaded one to null in order to allow garbage collection. This all seems a little cludgy.

protected override void LoadContent()
{
    Effect tempEffect = Content.Load("Effects/Default");
    effect = new DefaultEffect(tempEffect);
    tempEffect = null;

    ResetProjection();

    vertexDecl = new VertexDeclaration(new VertexElement[] {
            new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0)
        }
    );
}

protected override void UnloadContent()
{
    Content.Unload();
}

As of XNA 4.0 programmers no longer have access to the low-level shader architecture. We cannot set vertex and pixel shaders directly on the device, we cannot compile shaders at run-time...nada. So the only way to get a shader onto the device is via the Effect Framework. Since there's no longer a way to compile effects at run-time, the only way to get a custom effect into the game is either through the Content Pipeline or via cloning an existing effect. In later tutorials we'll look at writing custom processors so we can load our own effect classes. In the mean time, loading the effect and cloning it as a DefaultEffect (which IS-A effect) is a perfectly acceptable solution.

The last method we're going to look at in this class is the Draw method. The Draw method in this demo is only slightly more complex than it was in the previous tutorial. As seen in the code below I clear the back buffer, change a property on the effect class, apply the effect and then draw the primitives. This is done for both the triangle as well as the rectangle.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    effect.World = triangleTransform;
    effect.CurrentTechnique.Passes[0].Apply();

    GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip,
        triangleData, 0, 1, vertexDecl);

    effect.World = rectangleTransform;
    effect.CurrentTechnique.Passes[0].Apply();

    GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip,
        rectangleData, 0, 2, vertexDecl);
}

For those people who are coming from XNA 3.1 you should immediately notice the elegant simplcity of this method. Gone are the Effect.Begin() and Effect.End() methods, gone are the Pass.Begin() and Pass.End() methods, and gone is the CommitChanges() method. In their places is a single method: EffectPass.Apply().

EffectPass.Apply() is an elegant alternative to the previous paradigm. Now when you call EffectPass.Apply() it effectively calls Begin() and End() for you. Now, this isn't exactly what happens, but it's functionally the same. With the absence of Effect.Begin() it may seem as though there's no way to set effect-wide device states which might be necessary for the techniques you're trying to render. Nothing could be further from the truth. In addition to the new EffectPass.Apply() method, Microsoft has also added an overridable method on the Effect class called OnApply, which is called any time EffectPass.Apply() is called. This gives you the chance to push textures or do any other device setup you may want to do before Draw* routines are called. We won't be using the OnApply method in this tutorial, but we will a few tutorials from now when we start using textures.

The other thing missing (or rather moved) in this method is the need to set the GraphicsDevice.VertexDeclaration property. In XNA 4.0 Vertex Declarations are either bound with a Vertex Buffer, or is provided as part of the Draw* routine, as seen in the previous code listing. So it is no longer necessary to explicitly set the active vertex declaration before calling Draw.

Ok. We've finished the code for the Game1 class. Next is the DefaultEffect class. This is a fairly simple class as it's just an Effect-derived class that exposes a few EffectParameters as properties. One thing you may want to note is that in XNA 4.0 Microsoft has provided a few interfaces to help in the creation and use of custom effect classes. In this example I implement the IEffectMatrices interface, which just says I'll expose  World, View, and Projection matrices. As I mentioned earlier in the tutorial, we don't need a View matrix because the camera never moves from the origin. Thus, world and view space are the same. So in my little sample class I've just left the implementation of the View property blank. (or close to it) I would have left it off entirely if it wasn't necessary to implement the IEffectMatrices interface.

class DefaultEffect : Effect, IEffectMatrices
{
    EffectParameter world;
    EffectParameter projection;

    public DefaultEffect(Effect effect)
        : base(effect)
    {
        world = Parameters["World"];
        projection = Parameters["Projection"];
    }

    public Matrix World
    {
        get { return world.GetValueMatrix(); }
        set { world.SetValue(value); }
    }

    public Matrix View
    {
        get { return Matrix.Identity; }
        set { }
    }

    public Matrix Projection
    {
        get { return projection.GetValueMatrix(); }
        set { projection.SetValue(value); }
    }
}

As I said in the introduction, I want readers to become comfortable with writing their own shaders and effects. So in this tutorial series we will always write our own effects. Even if it does nothing more than draw white, unlit primitives. Which, incidentally, is all this little effect does.

float4x4 World;
float4x4 Projection;

float4 VertexShaderFunction(float4 input : POSITION0 ) : POSITION0
{
    float4 output = mul(input, World);    
    output = mul(output, Projection);

    return output;
}

float4 PixelShaderFunction(float4 input : POSITION0 ) : COLOR0
{
    return float4(1, 1, 1, 1);
}

technique Technique1
{
    pass Pass1
    {
        VertexShader = compile vs_2_0 VertexShaderFunction();
        PixelShader  = compile ps_2_0 PixelShaderFunction();
    }
}

To begin with, I declare two global variables, World and Projection, which are bound to the external EffectParamters. Then I define two functions, a vertex shader and a pixel shader. These shaders do the absolute minimum necessary to be called a real-time shader. The vertex shader just multiplies the input vector by the world matrix in order to put it in world space, and then multiplies it by the projection matrix to put it in projection space. That's it. The transformed position is then returned from the shader to be interpolated and processed by the pixel shader.

The pixel shader is even simpler. Regardless of the position passed in, it returns the color white for all pixels. The result of this effect, combined with the previous code can be seen in the following screenshot.

Download Project 2

(EDIT: There had been a problem with the Lesson02 Configuration initially which caused it not to build. The problem has been resolved, and your Lesson02.csproj should build just fine now)

Comments (13) -

Scott
Scott
4/15/2010 2:58:37 PM #

I don't mean to nitpick, but shouldn't this tutorial maybe be called "Drawing 2D Primitives" or "Drawing 2D Primitives in 3D space"?  I'm just saying, I was hoping for a cube and got a square Smile  I'm joking of course.

Great work!

Frank
Frank
11/15/2010 10:10:27 PM #

Jeromy,

Thank you for these wonderful tutorials.  I've always found myself wishing for NeHe like tutorials when learning anything new.  I always liked the way the built upon each other.

With this installment, I noticed a minor typo in your LoadContent() function.  The following line:

   Effect tempEffect = Content.Load("Effects/Default");

Should be:

   Effect tempEffect = Content.Load<Effect>("Effects/Default");

Keep up the great work!

Jannik
Jannik
11/19/2010 2:50:48 PM #

Thank you so much for this tutorial! It's great!

Pinetz
Pinetz
11/29/2010 5:48:46 AM #

Thank you for this tutorial! As the previos poster said its great!! Keep up the work.

Jeromy Walsh
Jeromy Walsh
12/8/2010 7:38:08 PM #

Depends on the App. Email me via the contact form at the top of the page if you're looking for a contractor.

Nick Janssen
Nick Janssen
1/31/2011 6:00:53 AM #

In the code:

            Effect tempEffect = Content.Load<Effect>("Effects/Default");
            effect = new DefaultEffect(tempEffect);
            tempEffect = null;

Does it really matter a big deal setting tempEffect to null? I mean, I haven't seen this way of working before in C#, since the garbage collector auto collects anyway when tempEffect loses scope. Am I not seeing something?

Other than that, great stuff!
Nick

PS: your captchas are insanely hard

Jeromy Walsh
Jeromy Walsh
1/31/2011 8:34:12 AM #

Hi Nick,

No, it does not matter. I put the tempEffect = null; there for two reasons. 1. I could potentially force the GC to clean up the memory quicker, and 2. To illustrate that we are allowing the GC to clean up that memory. Leaving scope would do the same thing, but wouldn't illustrate the point as much.

As for Recaptcha, all I do is include the JScript that references the recaptcha site. I don't actually control the recaptcha words.

Donovan Hughes
Donovan Hughes
8/14/2011 2:34:20 AM #

I'm a bit lost on the last box of text (being new to XNA and C#), where does this go within your program? Everywhere I seem to put it seems to be wrong. :S

I'll keep trying until someone can get back to me, hopefully I figure it out soon, good tutorial so far. Laughing

Donovan Hughes
Donovan Hughes
8/14/2011 2:54:48 AM #

Well found it, didn't take long, but at 4:30am, almost 5 am now... It just takes a bit longer to get things done / found, should have known it goes into an effects file. Like I said still new to this, hehe.

Donovan Hughes
Donovan Hughes
8/14/2011 10:10:59 PM #

Okay so I am reviewing this tonight trying to figure out how each thing works since I just kind of typed it out the other day and got it working.


Now I'm just a bit confused about this:

        protected void OnClientSizeChanged(object sender, EventArgs e)
        {
            ResetProjection();
        }

        protected void ResetProjection()
        {
            Viewport viewport = graphics.GraphicsDevice.Viewport;

            // Set the Projection Matrix
            effect.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,
                (float)viewport.Width / viewport.Height,
                0.1f,
                100.0f);

        }


I understand OnClientSizeChange is called and it calls ResetProjection();

But I follow it through: effct.Projection = all this math to create the perspective view... which leads down to:

public Matrix Projection
        {
            get { return projection.GetValueMatrix(); } // gets the current projection
            set { projection.SetValue(value); } // sets the new projection
        }


which as I understand is the get set for projection. So of course is called each time we press F11 or resize the window. What confuses me is it sets this:

EffectParameter projection;

I understand from reading the framework notes that this is a quicker way to send what they call "techniques" to the Effect Class. What I don't understand is what this is doing, because "EffectParameter projection;" as I understand is just set, where is it used by the Effects Class?

Donovan Hughes
Donovan Hughes
8/19/2011 9:44:14 PM #

Disregard what I asked. I spent some time reading about the Direct3D pipeline for a few days and understand this a whole lot better. Great tutorial, moving on to the next one.

dyork11
dyork11
9/10/2011 6:17:04 PM #

Still following along...

Minor gripes:
1. Missing the critical steps of adding a class and an effect; where to to put them; and the usings needed in the class.
2. The use of tempEffect is plain wrong. The GC picks it up when it goes out of scope, and if that isn't enough then you call Dispose(). Setting to null does nothing.

I've written a lot of C# code, so couple of comments.
1. Instance variables: I prefer leading underscore -- they need to look different from locals
2. In Initialize(): Array initialisers are a better idiom
3. If you create the class/method before referring to it, it works nicer with the built in syntax checking.

Marc Angers
Marc Angers
10/15/2011 8:46:58 AM #

The current link to the source code is incorrect. You'll find below the correct link.

http://gamedevelopedia.com/downloads/Lesson02.zip



About the author

Jeromy Walsh is a professional game programmer with multiple credited and uncredited AAA game titles. Jeromy's primary area of expertise is in Tools/Engine development, though he likes to fiddle with other areas as well.

Month List