NeHe Tutorial #3: Coloring 3D Primitives

16. April 2010 12:56 by Jeromy Walsh in Tutorials  //  Tags: ,   //   Comments (2)

In tutorial #3 we're going to be duplicating a lot of the code which was written in tutorial #2 in order to implement the same triangle and quad as last time; but this time with vertex and diffuse material COLOR! As I've stated in the last few tutorials, part of my goal for this series of tutorials is to help aspiring XNA programmers become more familiar with Real-Time Shaders and HLSL. As a result, rather than just using the BasicEffect class which has its own DiffuseColor and VertexColorEnabled properties, we're going to be doing the vertex and material coloring ourselves; using our own custom effect and our own custom DefaultEffect class.  So let's get started!

As usual, the first change we're going to make to the Game1 class is to add a few fields. As with tutorial #2, we're going to add an instance of the DefaultEffect class, which we'll be implementing ourselves, in addition to a Vertex Declaration and vertex data for our triangle and rectangle.  

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

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

	VertexPositionColor[] triangleData;
	Vector3[] rectangleData;

The main thing to note about these fields is the difference between the triangle and rectangle data. In Tutorial #2, both the triangle and the rectangle were created using Vector3. In this tutorial we plan to color the triangle with vertex coloring. In order for the video card to know what color each vertex will be we must supply it with not only a position, but a color as well. So a simple Vector3 is no longer sufficient. In general, when creating vertices which contain more than just positions, we must create a custom structure comprised of all the data associated with each individual vertex. But, as with XNA 3.1 (and before), XNA provides built-in structures for many of the most common vertex formats. Each one is named after the data it contains and in this case, we're going to utilize the VertexPositionColor structure in order to send the video card a position and a color for each vertex.

The next part of the code is a repeat from Tutorial 2. This tells the Game Window that any time the window is resized we want to adjust the projection matrix so our objects can be drawn with the new aspect ratio.

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

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

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

The next method we're going to look at is the Initialize method. As with Tutorial #2, we're essentially doing two things here. The first thing we're going to do is initialize two matrices which will act as the world transformations for both our triangle and rectangle. For an explanation of why the transformations have a -6.0f z-coordinate, and why we have no View transformation, see Tutorial #2.

protected override void Initialize()
{
	triangleTransform = Matrix.CreateTranslation(new Vector3(-1.5f, 0.0f, -6.0f));
	rectangleTransform = Matrix.CreateTranslation(new Vector3(1.5f, 0.0f, -6.0f));

	triangleData = new VertexPositionColor[3];
	triangleData[0] = new VertexPositionColor(new Vector3(1.0f, -1.0f, 0.0f),  Color.Red);
	triangleData[1] = new VertexPositionColor(new Vector3(-1.0f, -1.0f, 0.0f), Color.Green);
	triangleData[2] = new VertexPositionColor(new Vector3(0.0f, 1.0f, 0.0f),   Color.Blue);

	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();
}

The second thing we do in the Initialize method is to create two data arrays. The first is a set of three VertexPositionColor structures which will be used as the vertices for our triangle. You can see that we initialize both a Vector3 (for the position) as well as a color for each vertex. This is unnecessary in the quad, because while we do intend on coloring it, using a diffuse material color, the color does not change per-vertex. As such, it's not necessary to specify the color on each vertex. As you'll see shortly, we'll just pass the diffuse material color to the device directly, using effect parameters.

With the Initialize method out of the way it's time to look at the LoadContent method.

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)
		}
	);

	effect.DiffuseColor = new Vector3(0f, 0f, 1.0f);
}

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

LoadContent looks almost identical to that which was provided in Tutorial #2. The only difference is the inclusion of the final line of the method. This last line tells the effect what the diffuse material color is for our primitives. When we get to the code listing for the DefaultEffect you'll see how this is transferred to the vertex shader.

The final method of the Game1 class is the Draw method. This again is almost identical to the Draw method discussed in Tutorial #2 with two differences - one subtle, and one less subtle.

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

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

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

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

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

The first difference are the assignments of IsVertexColoringEnabled. On the third line of the method we set this value to true so the device knows which vertex shader to use (the one which uses vertex coloring). We reset this value back to false on line 7 so that our quad does not use the per-vertex coloring. Instead, its behavior is to use the vertex and pixel shaders which access the global material color set by our custom DefaultEffect class.

The other, more subtle difference between this Draw method and that provided in Tutorial #2, is the first call to DrawUserPrimitives. This invocation has two changes. First, the generic method now applies to type VertexPositionColor rather than Vector3. This tells the method what type of vertices are going to be pushed to the device. The other difference is the passed in VertexDeclaration. Instead of passing in our custom vertex declaration "vertexDecl", we pass in VertexPositionColor.VertexDeclaration. Note: Unlike XNA 3.1, in which each of the custom vertex format structures contained an array of VertexElements, XNA 4.0 cuts out the middle man and each custom vertex format explicitly provides a VertexDeclaration. This is a lot more convenient, as it means when using the built-in vertex formats, you don't even need to create a vertex declaration.

Ok, with Game1 out of the way it's time to look at our DefaultEffect class. This again resembles what was created in Tutorial #2, with the addition of a few new EffectParameters and matching properties.

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

	bool isVertexColoringEnabled;
	
	public DefaultEffect(Effect effect)
		: base(effect)
	{
		world        = Parameters["World"];
		projection   = Parameters["Projection"];
		shaderIndex  = Parameters["ShaderIndex"];
		diffuseColor = Parameters["DiffuseColor"];
	}

	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); }
	}

	public Vector3 DiffuseColor
	{
		get { return diffuseColor.GetValueVector3(); }
		set { diffuseColor.SetValue(value); }
	}

	public bool IsVertexColoringEnabled
	{
		get { return isVertexColoringEnabled; }
		set
		{
			isVertexColoringEnabled = value;
			if (isVertexColoringEnabled)
				shaderIndex.SetValue(1);
			else
				shaderIndex.SetValue(0);
		}
	}
}

The constructor continues to initialize the object by accessing the parameters from the real-time shader. In this case, it also grabs the shaderIndex and diffuseColor parameters. The first is going to be used to tell the device which vertex and pixel shaders to use, and the second will be used by the default pixel shader to tell it what color to return for each pixel.

You can see that the DiffuseColor property follows the same pattern as the transformation matrices, with get and set methods which return the effect parameter value or set it on the device. The IsVertexColoringEnabled property is a little different. Rather than using bool, this property internally maps the bool to an index, either 0 or 1, which will act as an array index to identify which of a set of shaders the effect framework should set on the device. This is an alternative approach to using if-statements in the vertex and pixel shaders as it is only performed once, when setting the shaders on the device, rather than per-vertex or worse, per-pixel.

Since we're talking about the shaders, let's take a look at the .fx file now.

float4x4 World;
float4x4 Projection;
int ShaderIndex = 0;
float3 DiffuseColor;

struct VertexPositionColor
{
    float4 Position : POSITION0;
	float4 Color    : COLOR0;
};

float4 DiffuseVertexShader(float4 input : POSITION0 ) : POSITION0
{
    float4 output = mul(input, World);    
    return mul(output, Projection);    
}

float4 DiffusePixelShader(float4 input : POSITION0 ) : COLOR0
{
    return float4( DiffuseColor, 1.0 );
}

VertexPositionColor VertexColorVertexShader(VertexPositionColor input)
{
    VertexPositionColor output;

    output.Position = mul(input.Position, World);    
    output.Position = mul(output.Position, Projection);
	output.Color    = input.Color;

    return output;
}

float4 VertexColorPixelShader(VertexPositionColor input) : COLOR0
{
    return input.Color;
}

VertexShader VertexShaders[2] =
{
	compile vs_2_0 DiffuseVertexShader(),
	compile vs_2_0 VertexColorVertexShader(),
};

PixelShader PixelShaders[2] =
{
	compile ps_2_0 DiffusePixelShader(),
	compile ps_2_0 VertexColorPixelShader()
};

technique DefaultEffect
{
    Pass
    {
        VertexShader = (VertexShaders[ShaderIndex]);
        PixelShader  = (PixelShaders[ShaderIndex]);
    }
}

This effect file is a bit more complex than that previously implemented. First, it provides additional global variables, ShaderIndex and DiffuseColor, which provide access to the EffectParamters utilized on the CPU. The other difference is the addition of a new structure called VertexPositionColor. You'll note that it shares the same name as the structure used in the C# code, and refers to the vertex format used by the vertex coloring shaders. I could have chosen another name, but I find that describing the vertex structure in the shader by format prevents the common habit of creating multiple structures with the same format for different purposes, such as vertexformat_in and vertexformat_out.

Unlike the previous effect file this one contains two methods which will be used as vertex shaders, and two methods which will be used as pixel shaders. In each case, there is one method that implements per-vertex coloring, and one that performs simple diffuse material coloring.

The first set of methods are the diffuse material methods. The DiffuseVertexShader method is identical to that implemented in Tutorial #2 as it only acts to transform the position of the vertex into projection space, and then pass it off to be interpolated and processed by the pixel shader. The DiffusePixelShader is similar to that provided in Tutorial #2, which only returned the color white, but this one instead returns the color provided in the global variable DiffuseColor. The idea being that it returns each pixel as whatever color was specified in the C# code.

The next method in the file is VertexColorVertexShader. This vertex shader again transforms the position into projection space, but it additionally sets the output color to the input color. For those not already familiar with the relationship between vertex and pixel shaders. Any data which is returned out of the vertex shader will be interpolated across the surface of the primitive to be processed per-pixel in the pixel shader. Because each of our vertices have different colors, and because we're returning it to the device for interpolation, it will result in each pixel in the pixel shader being sent an interpolated color (a blend of the three vertex colors). We can then use this in the pixel shader to determine the color of the pixel.

The VertexColorPixelShader function is the next in the .fx file. You can see that it has precisely one line, returning the input color back to the caller. This may seem a little silly, but remember that the caller has no idea what the pixel-shader is going to do. It only knows the inputs being sent to the pixel shader and the output - always a float4, representing the color of the pixel. If we wanted to we could return the interpolated vertex color weighted by the position. Or, we could completely ignore the color and still return a diffuse material color. In any event, the method just returns the interpolated color for the pixel back to the device to be used as the pixel color.

The final part of the effect file may seem a bit strange. We declare two arrays, one of vertex shaders, and one of pixel shaders, with the individual elements being equal to the compiled functions. This serves to give us a way to refer to the shaders by indices. If you look at the technique definition you'll see that in Pass 0 (the only pass), the vertex shader and pixel shader on the device are set to one of the indices of each of the two arrays, determined by the ShaderIndex constant. Remember what I said before. We could have just put if-statements in a single vertex and pixel shader, ignoring the vertex-coloring data when it was unnecessary, but this method is a bit cleaner when you've got a large number of vertex and pixel shaders, such as is done in BasicEffect. In future effect files I'll probably just use if-statements for compactness.

If you enter all of the previous code into the starter game shown in Tutorial #1, or just download the completed project below and compile/run it, you should see the following:

Hopefully it's not a surprise that the triangle is colored via an interpolation of the three vertex colors, while the rectangle is colored by the single diffuse material color we passed in as a shader constant. If you want to play with this project more, feel free to change the diffuse color passed in by the effect, or change the vertex colors on each of the vertices.

Download Project #3

Comments (2) -

Robert
Robert
12/2/2010 4:39:13 AM #

Thanks for tutorials, clean and simple, work's great, keep up good work.

Jamison
Jamison
1/31/2011 7:16:35 PM #

I was surprised that the triangle is colored via an interpolation Smile Can you recommend some more basic tutorials that are as a good of a quality as yours? Also how would you change VertexColorPixelShader by the position? I'm lost without intellisense here.

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