What about drawing lines, boxes or other simple 2d stuff?
Oh no, this is one of the parts that is really annoying and this does require some rethinking because all this kind of rendering was done with help of the fixed function pipeline (hello DirectX 7), which is no longer supported in XNA. It was never easy in DirectX to draw lines or filled boxes, you had to create your own vertex buffer or just draw some primitives and set all kinds of renderstages, but with XNA this gets even more complicated. Better not tell anyone how simple this is in OpenGL (glLine anyone ^^).
So how can we draw some lines in 3d?
Again: It is very important to know that you have to do EVERYTHING with shaders! The Sprite classes are just some helpers, but for 3d you need shaders, nothing else will work! I say this that often because many people in the XNA Forum already having problems rendering data and do not understand that if you don't have a shader around your rendering code, nothing will happen! Additionally you have to make sure you pass all the required data to your shader, there is a lot more work involved that just rendering some primitives in DirectX using the fixed function pipeline.
I will try to keep this as short as possible, but you will see this is quite a lot of code for just rendering a single line. Here we go. We start with the variables we need (add to your Game class):
Matrix Projection = Matrix.Identity,
View = Matrix.Identity;
Vector3 pos1 = new Vector3(0, 0, 0),
pos2 = new Vector3(100, 100, 100);
VertexPositionColor[] lineVertices =
new VertexPositionColor[2];
lineVertices[0] = new VertexPositionColor(pos1, Color.Red);
lineVertices[1] = new VertexPositionColor(pos2, Color.Yellow);
Effect effect = null;
EffectParameter worldViewProj = null;
We will use the Projection and View to calculate the worldViewProj matrix for our shader to convert the 3d coordinates to 2d (yep, I told you, we have to do everything ourself). If you want some camera control, always update the View matrix, which again you have to manage yourself (hello XNA team, some help with this basic stuff plz). Ok, lets continue before I explode, here is the initialization code (copy to your Game constructor):
float aspectRatio = (float)TestGame.Width / (float)TestGame.Height;
Projection = Matrix.CreatePerspectiveFieldOfView(
(float)Math.PI / 2, aspectRatio, 0.1f, 1000.0f);
View = Matrix.CreateLookAt(
new Vector3(0, 0, -50), Vector3.Zero, Vector3.Up);
CompiledEffect compiledEffect = Effect.CompileEffectFromFile(
"Shaders\\LineRendering.fx", null, null, CompilerOptions.None,
TargetPlatform.Windows);
effect = new Effect(TestGame.Device,
compiledEffect.GetShaderCode(), CompilerOptions.None, null);
worldViewProj = effect.Parameters["worldViewProj"];
Ok, what happens here? First of all we calculate our aspect ratio, nothing special. Then we have to calculate our projection and view matrices (please read the DirectX documentation if you have no idea what I'm talking about). Basically we have a field of view of 90 degrees (PI/2), we use our aspect ratio and we have a view range from 0.1 (near) to 1000.0 units (far). Then we create our camera at the position (0, 0, -50) looking at the center of our scene.
Next we have to load our shader effect file. Yes, I told you about that earlier, we need a freaking shader to draw our simple line. We will get into that in a little bit, but ALWAYS make sure the .fx file is correct (test with FX Composer first). If the .fx file contains compiler errors you get unfunny NullReferenceExceptions, which won't help you (see below). Finally we are getting the worldViewProj parameter. This is the only line of this whole line code I really appreciate. Getting shader parameters is now a lot easier and cleaner, good work here! Also working with shaders is very easy now.
Time to continue with our code, we need the rendering code (add to Draw()) now:
// Start line shader
effect.Begin(EffectStateOptions.Default);
effect.Techniques[0].Passes[0].Begin();
// Render line
worldViewProj.SetValue(View * Projection);
TestGame.device.VertexDeclaration = new VertexDeclaration(
TestGame.device, VertexPositionColor.VertexElements);
TestGame.device.DrawUserPrimitives
(
PrimitiveType.LineList, 1, lineVertices);
// End shader
effect.Techniques[0].Passes[0].End();
effect.End();
This code is pretty straight forward. We start our shader and select the first technique (the only one we got). Then we calculate our worldViewProj matrix from the View and Projection matrices we calculated in the constructor. After setting the VertexDeclaration we can draw our primitives, which are lines in our case. Just one to be more specific. Adding more lines is quite easy at this point. Finally we have to close the rendering pass (we just got 1 pass, else we would have to make a foreach loop around this code) and we also close the shader.
So far so good, but how does the rendering now happen? 100% in the shader, the code we just wrote will do nothing more than calling the shader with the data we set, the line point positions and colors. So lets take a look at the shader, which does all the rendering. The shader itself is quite simple:
// File: LineRendering.fx, Author: Abi
// Code lines: 52, Size of file: 1,18 KB
// Creation date: 31.08.2006 05:36
// Last modified: 31.08.2006 06:44
// Generated with Commenter by abi.exDream.com
// Note: To test this use FX Composer from NVIDIA!
string description = "Line rendering helper shader for XNA";
// Default variables, supported by the engine
float4x4 worldViewProj : WorldViewProjection;
struct VertexInput
{
float3 pos : POSITION;
float4 color : COLOR;
};
struct VertexOutput
{
float4 pos : POSITION;
float4 color : COLOR;
};
VertexOutput LineRenderingVS(VertexInput In)
{
VertexOutput Out;
// Transform position
Out.pos = mul(float4(In.pos, 1), worldViewProj);
Out.color = In.color;
// And pass everything to the pixel shader
return Out;
} // LineRenderingVS(VertexInput In)
float4 LineRenderingPS(VertexOutput In) : Color
{
return In.color;
} // LineRenderingPS(VertexOutput In)
// Techniques
technique LineRendering
{
pass Pass0
{
VertexShader = compile vs_1_1 LineRenderingVS();
PixelShader = compile ps_1_1 LineRenderingPS();
} // Pass0
} // LineRendering
And thats it. As you can see we just take the input position, transform it and then output the color we interpolated through the vertex shader. The pixel shader has just to output the color. Working with other primitive types can be done in a similar way, so I hope this code helps.
And what about 2d lines?
2D lines can be done quite similary. In DirectX you would never think about rendering 2D lines with shaders, when it is so much easier with the fixed function pipeline. But this is not possible in XNA, so we have to use shaders again. Lets go quickly through the code that is required:
Matrix Projection = Matrix.Identity,
View = Matrix.Identity;
Point pos1 = new Point(0, 0),
pos2 = new Point(500, 250);
VertexPositionColor[] lineVertices =
new VertexPositionColor[2];
Effect effect = null;
EffectParameter worldViewProj = null;
Very similar to what we had above, only our positions are now Points and not Vector3. Also note we don't calculate the lineVertices here because the data is resolution dependant and we have to grab the resolution from the graphics object first. Lets take a look at the initialization code:
float aspectRatio = (float)TestGame.Width / (float)TestGame.Height;
Projection = Matrix.CreatePerspectiveFieldOfView(
(float)Math.PI / 2, aspectRatio, 0.1f, 1000.0f);
View = Matrix.CreateLookAt(
new Vector3(0, 0, -50), Vector3.Zero, Vector3.Up);
lineVertices[0] = new VertexPositionColor(
new Vector3(
-1.0f + 2.0f * pos1.X / TestGame.Width,
-(-1.0f + 2.0f * pos1.Y / TestGame.Height), 0), Color.Red);
lineVertices[1] = new VertexPositionColor(
new Vector3(
-1.0f + 2.0f * pos2.X / TestGame.Width,
-(-1.0f + 2.0f * pos2.Y / TestGame.Height), 0), Color.Green);
CompiledEffect compiledEffect = Effect.CompileEffectFromFile(
"Shaders\\LineRendering2D.fx", null, null, CompilerOptions.None,
TargetPlatform.Windows);
effect = new Effect(TestGame.Device,
compiledEffect.GetShaderCode(), CompilerOptions.None, null);
worldViewProj = effect.Parameters["worldViewProj"];
This looks slightly more complex. The reason for that is the conversation from pixel coordinates to screen space, which goes from -1 to +1 and has y inverted. We could also do this calculation in the vertex shader, but I like C# more ^^ Please also note that we don't need the z coordinate, but it is way easier to just use the VertexPositionColor struct instead of creating our own struct. Lets continue with the rendering.
// Start line shader
effect.Begin(EffectStateOptions.Default);
effect.Techniques[0].Passes[0].Begin();
// Render line
worldViewProj.SetValue(View * Projection);
TestGame.device.VertexDeclaration = new VertexDeclaration(
TestGame.device, VertexPositionColor.VertexElements);
TestGame.device.DrawUserPrimitives
(
PrimitiveType.LineList, 1, lineVertices);
// End shader
effect.Techniques[0].Passes[0].End();
effect.End();
Nothing changed here, nothing we have to discuss. Everything works just the same. lets take a look at the shader!
// File: LineRendering2D.fx, Author: Abi
// Code lines: 52, Size of file: 1,18 KB
// Creation date: 31.08.2006 05:36
// Last modified: 31.08.2006 06:55
// Generated with Commenter by abi.exDream.com
// Note: To test this use FX Composer from NVIDIA!
string description = "Line rendering in 2D space shader for XNA";
// Default variables, supported by the engine
float4x4 worldViewProj : WorldViewProjection;
struct VertexInput
{
float3 pos : POSITION;
float4 color : COLOR;
};
struct VertexOutput
{
float4 pos : POSITION;
float4 color : COLOR;
};
VertexOutput LineRendering2DVS(VertexInput In)
{
VertexOutput Out;
// Transform position
Out.pos = float4(In.pos, 1);
Out.color = In.color;
// And pass everything to the pixel shader
return Out;
} // LineRendering2DVS(VertexInput In)
float4 LineRendering2DPS(VertexOutput In) : Color
{
return In.color;
} // LineRendering2DPS(VertexOutput In)
// Techniques
technique LineRendering2D
{
pass Pass0
{
VertexShader = compile vs_1_1 LineRendering2DVS();
PixelShader = compile ps_1_1 LineRendering2DPS();
} // Pass0
} // LineRendering2D
Not much changed here either. Only the vertex shader is changed, we don't have to use our worldViewProj matrix anymore. We are just copying the data over to the pixel shader for rendering the screen stuff directly.
Thats it for rendering 2D data. I guess most 2D stuff will be covered by the Sprite class, but if you need some custom 2d stuff (lines, boxes, etc.) this code should help you out. Have fun writing more complex stuff.