FW: http://www.stromcode.com/2008/04/02/an-introduction-to-hlsl-part-i/
An Introduction to HLSL (Part I)
In a previous article we looked at how to use custom shaders when rendering models, or as post-processing effects when applied to an entire scene. Now let’s look at how to build our own shaders.
<script type="text/javascript">
</script> <script src="http://pagead2.googlesyndication.com/pagead/show_ads.js" type="text/javascript"> </script> width="468" scrolling="no" height="15" frameborder="0" allowtransparency="true" hspace="0" vspace="0" marginheight="0" marginwidth="0" src="http://pagead2.googlesyndication.com/pagead/ads?client=ca-pub-4918349833220447&dt=1212402159164&lmt=1212402158&prev_slotnames=5585453236&output=html&slotname=2273068045&correlator=1212402158698&url=https%3A%2F%2F2.zoppoz.workers.dev%3A443%2Fhttp%2Fwww.stromcode.com%2F2008%2F04%2F02%2Fan-introduction-to-hlsl-part-i%2F&ref=https%3A%2F%2F2.zoppoz.workers.dev%3A443%2Fhttp%2Fwww.stromcode.com%2Fcategory%2Fxna%2F&frm=0&cc=100&ga_vid=2992875446891075600.1212401973&ga_sid=1212401973&ga_hid=1429208539&ga_fc=true&flash=0&u_h=900&u_w=1440&u_ah=870&u_aw=1440&u_cd=32&u_tz=480&u_his=1&u_java=true&u_nplug=13&u_nmime=40" name="google_ads_frame">
For this project, I’ll be using the same code from the Intro to Effects and Post-Processing article. This code is already set up to drop effects on models and to use effects as post-processors. So you’ll need to already know how to use effects in both contexts in order to follow along.
We’ll move through the non-HLSL stuff quickly. First, add a new effect file to your project. I keep all of mine in a content folder called FX. XNA builds a simple effect file template for you.
Let’s start with this template. If you build and run your project, you see something less than stellar.
Our first boring effect
So let’s begin with this simple effect file and try to figure out what it’s doing.
First of all, every effect file consists of a vertex shader, and a pixel shader. In short, the vertex shader is able to manipulate a vertex property, like its location and its color. The pixel shader is receives the output of the vertex shader, and is able to perform per-pixel operations, which can mean lighting, coloring, texturing, multi-texturing, and so on.
This represents two of the three stages in the pixel processing pipeline. The missing stage, the geometry stage, sits between these two. XNA does not support geometry shaders, so we don’t need to worry about this stage. All we need to know is what happens between the vertex and pixel shader stages. Basically, once the vertices get transformed, colored, and so on by the vertex shader, the rasterizer converts the triangles in our primitives to pixels ready for the screen. Our pixel shader outputs the color of each pixel.
So if we look now at our default effect file, we can start tearing it apart.
Let’s look at the header first.
-
-
float4×4 World;
-
-
float4×4 Projection;
-
This is where we specify what parameters we need passed in to our effect. Remember, that looks like this:
-
-
myEffect.
Parameters
[
"World"
].
SetValue
(world
);
-
myEffect.
Parameters
[
"View"
].
SetValue
(view
);
-
myEffect.
Parameters
[
"Projection"
].
SetValue
(projection
);
-
That sets up the matrices that the vertex shader is going to need to to translate the raw vertices to their final, screen coordinates. Also note that the HLSL float4×4 and the XNA Matrix types are equivalent. We don’t need to do anything special to get our Matrix values ready for HLSL.
Next we’ve got a couple struct definitions:
-
-
struct VertexShaderInput
{
-
-
};
-
-
struct VertexShaderOutput
{
-
-
};
-
Here we simply specify what the input and output of our vertex shader looks like. XNA will automatically pass in the correct values, and the correct amount of values, here. In this default effect, we’re only asking for the vertex position. We’ll use our three matrices to translate this position and then return the translated position. Consequently, our input and output structs are identical. Note here that a HLSL float4 type is identical to an XNA Vector4 type. Our vertex shaders may also process color. In that case, you’d want to add a float4 to the input and output structs for a RGBA color value.
After the struct definitions, we actually define out vertex shader function.
-
-
VertexShaderOutput VertexShaderFunction
(VertexShaderInput input
)
{
-
VertexShaderOutput output;
-
float4 worldPosition = mul
(input.
Position, World
);
-
float4 viewPosition = mul
(worldPosition,
View
);
-
output.
Position = mul
(viewPosition, Projection
);
-
-
return output;
-
}
-
This is quite simple. We define a function that takes a VertextShaderInput struct as a parameter, and returns a VertexShaderOutput struct. Internally, it multiplies — via the mul() function — our vertex by the world, view and projection matrices, and returns the resulting vertex location. That’s it. This is something you’ve done using BasicEffect a bunch of times, and here’s what it looks like in HLSL.
Next, we define the pixel shader function:
-
-
float4 PixelShaderFunction
(VertexShaderOutput input
) : COLOR0
{
-
return float4
(
1,
0,
0,
1
);
-
}
-
Our pixel shader function returns a float4 (we could have defined this as a struct called PixelShaderOutput or something similar if we wanted). There is one new thing here though, the COLOR0 at the end of the function header. In fact, we saw that earlier, with POSITION0. What does that mean?
These are semantics. They basically define common elements between our game code, and our HLSL. Remember I said that the vertex position would get automatically passed in to our vertex shader without us having to do anything special? This is because the POSITION0 is a semantic meaning vertex position. The 0 at the end means, “the first one”. We can define POSITION1, COLOR3, or whatever depending on how many inputs to the function we’re going to have.
As for the pixel shader function, can you figure out what it does? I basically ignores the pixel position returned by the vertex shader, ignores whatever color data was associated with that pixel (since we never received the colors of the vertices, we couldn’t have done anything with it anyways), and returns a color value. In this case, it returns pure red with alpha 1.0.
The last step is to wrap this up into a technique, and a pass for that technique:
-
-
technique Main
{
-
pass Pass0
{
-
VertexShader = compile vs_1_1 VertexShaderFunction
(
);
-
PixelShader = compile ps_1_1 PixelShaderFunction
(
);
-
}
-
}
-
We have one technique, called Main, which has one pass, called Pass0. And then we just tell it to compile a Vertex Shader 1.1 and a Pixel Shader 1.1 out of the two functions we’ve defined for each.
Well, that is the most basic introduction we could possibly do. We looked at the bare minimum of vertex and pixel shaders and made sense of the most basic effect file imaginable. In part 2, we’ll go a little deeper and actually make a shader that does something.
An Introduction to HLSL (Part II)
Continuing on in our exploration of HLSL, let’s look at building a simple shader that applies a texture to a model using UV coordinates.
<script type="text/javascript">
</script> <script src="http://pagead2.googlesyndication.com/pagead/show_ads.js" type="text/javascript"> </script> width="468" scrolling="no" height="15" frameborder="0" allowtransparency="true" hspace="0" vspace="0" marginheight="0" marginwidth="0" src="http://pagead2.googlesyndication.com/pagead/ads?client=ca-pub-4918349833220447&dt=1212402155508&lmt=1212402154&prev_slotnames=5585453236&output=html&slotname=2273068045&correlator=1212402155323&url=https%3A%2F%2F2.zoppoz.workers.dev%3A443%2Fhttp%2Fwww.stromcode.com%2F2008%2F04%2F03%2Fan-introduction-to-hlsl-part-ii%2F&ref=https%3A%2F%2F2.zoppoz.workers.dev%3A443%2Fhttp%2Fwww.stromcode.com%2Fcategory%2Fxna%2F&frm=0&cc=100&ga_vid=2992875446891075600.1212401973&ga_sid=1212401973&ga_hid=336651669&ga_fc=true&flash=0&u_h=900&u_w=1440&u_ah=870&u_aw=1440&u_cd=32&u_tz=480&u_his=1&u_java=true&u_nplug=13&u_nmime=40" name="google_ads_frame">
Before we get to anything super complicated, we still have a couple basics to cover that we didn’t touch on in part 1. Now we’ll look at passing textures to our shader, using our model’s UV coordinate map to apply that texture to our model, and setting up a couple render states. This isn’t all that complicated, although finding out exactly how to do this is a little tough.
To start it off, let’s look at our HLSL:
-
-
float4×4 World;
-
-
float4×4 Projection;
-
texture myTexture;
-
-
sampler2D mySampler = sampler_state
{
-
Texture =
(myTexture
);
-
MinFilter = Linear;
-
MagFilter = Linear;
-
AddressU = Clamp;
-
AddressV = Clamp;
-
};
-
-
struct VertexShaderInput
{
-
-
-
float2 TexCoord: TEXCOORD0;
-
float3 tNormal: NORMAL;
-
};
-
-
struct VertexShaderOutput
{
-
-
-
float2 TexCoord: TEXCOORD0;
-
};
-
-
VertexShaderOutput VertexShaderFunction
(VertexShaderInput input
)
{
-
VertexShaderOutput output;
-
float4 worldPosition = mul
(input.
Position, World
);
-
float4 viewPosition = mul
(worldPosition,
View
);
-
output.
Position = mul
(viewPosition, Projection
);
-
-
-
output.
TexCoord = input.
TexCoord;
-
-
return output;
-
}
-
-
float4 PixelShaderFunction
(VertexShaderOutput input
) : COLOR0
{
-
float4 output;
-
output = tex2D
( mySampler, input.
TexCoord
);
-
//output.r = 1 - output.r;
-
//output.g = 1 - output.g;
-
//output.b = 1 - output.b;
-
output.
a =
1;
-
-
return output;
-
}
-
-
technique Main
{
-
pass Pass0
{
-
ZENABLE =
TRUE;
-
ZWRITEENABLE =
TRUE;
-
CULLMODE = CCW;
-
VertexShader = compile vs_1_1 VertexShaderFunction
(
);
-
PixelShader = compile ps_1_1 PixelShaderFunction
(
);
-
}
-
}
-
That’s the whole thing. What’s different?
First of all, we’ve got a new parameter in the header: a texture. The HLSL texture type maps directly to the XNA Texture2D type. We also have a sampler2D variable that is defined with a sampler_state block. All we really need to know here is that the sampler2D is the thing we’ll be getting our texture data from. Presumably we could set up a couple different sampler2D objects with completely different sampler_states but which reference the same texture. We can also pass in non-texture data in a texture, but that’s beyond our little exploration for now.
Now, we’ve changed the definition of our VertexShaderInput and VertexShaderOutput. We’ve done this because we need a few more things from XNA in order to texture. Most importantly, we need the uv coordinates of each vertex, so we add float2 TexCoord: TEXCOORD0; to our struct to make sure we get it.
At this point, we’ve got our texture, and we have the uv coordinates for our vertices. Our vertex shader doesn’t need to do anything with these, but it does need to pass them along to the pixel shader, which will do something with them. That’s why the VertexShaderInput and VertexShaderOutput are nearly identical.
So let’s look at the pixel shader, since that’s where the texturing actually happens. Remember, the pixel shader has one job: to return the color of a pixel. In our case, the color we need is stored at the uv coordinate of the texture, so we just need a way to grab that pixel and look at it’s color. HLSL has a function that does this for us:
tex2D(mySampler, input.TexCoord);
This is pretty simple. This function looks at the first parameter, a sampler, and returns a float4 containing the RGBA values of the pixel at location input.TexCoord, which is a 2D vector containing the UV coordinates of the pixel. Just return this value, and we’re done.
Woops! No backface culling!
If we stopped here, we’d have one small problem. Take a look at the picture to the right and see what I mean.
It might not be totally obvious from the picture what’s going on, but there is no backface culling going on. Which means the triangles which should be obscured by those triangles closest to us are getting drawn anyways. HLSL by default does no culling. So everything gets drawn, even if it overlaps something else. To fix this, we need to set a few render states. As you can see from the Pass0 body, we set the culling mode to CCW (counter clockwise) and enable z-buffering. Now everything works right.
Well, there wasn’t much to it but that’s our second shader. Just to make it a little more interesting, I included some commented out code to manipulate the texture color in the pixel shader. Uncomment those three lines to mess with the colors, and invert the image.
That’s all for part 2 of the Intro to HLSL series. Now I’ll have to figure out what to do in part 3. If you have any suggestions, feel free to let me know.