|
|
| SHADERS - Introduction Introducing the Introduction I'm not an expert on High Level Shader Language (HLSL) by any means, and that's probably a good thing for those of you who don't know much about it either. When I first started programming shaders, I had a hard time finding anything that could really explain it at a level that I could understand. This introduction isn't going to go into the graphics pipeline at all, you'll have to search elsewhere for that level of detail. Instead, I'm going to go into the very basics of a shading program and give you an idea of how everything fits together in the bigger picture.
What is a shader?
Shaders are simply a means of manipulating data in some way to get a desired result. They operate on two types of data: vertices and pixels. There are two types of shaders dedicated to each: vertex and pixel shaders. Vertex shaders modify vertex positions and other data at the vertex level. Pixel shaders take data between the vertices by interpolating the values between each pixel. The final result is the color of that pixel. Shaders are powerful because you can tell them exactly how you want to manipulate each vertex and each pixel. With all that power comes a lot of complexity, so I'm going to try to break it down a bit so the task doesn't look so daunting. I was afraid of shaders for a while, but eventually I embraced them because they really are quite elegant and beautiful once you get your head around them.
.FX Files
Pronounced "effects" files, you store you shaders inside of these files which get loaded inside of your program. In your XNA code, you'll specify which shaders from the file you want to use by specifying a technique, which will be explained later. Usually you'll have several effect fiels in your project since you'll be using many different types of shaders for different scenarios.
Shader Format
Here's an example structure of a shader file:
float var1; // Variable Declaration float var2; ...
struct VSInput // Structure defining what data your vertex shader needs passed into it { }; struct VSOutput // Structure defining what data you vertex shader outputs to the pixel shader. { };
// The vertex shader VSOutput VertexShader( VSInput inputData ) { } // The pixel shader float4 PixelShader( VSOutput inputData ) { } // The technique gives you a way of selecting which shaders you want to use and in // what combination in case you have more in one file. This technique will expand // to include several different passes referencing different vertex and pixel shaders // when you get into more advanced shaders. technique Diffuse { pass Pass0 { VertexShader = compile vs_1_1 VertexShader(); PixelShader = compile ps_1_1 PixelShader(); } }
Now most of that may seem like complete jibberish to you, but don't worry, I'll start explaining it piece by piece.
Variable Declaration Just like in a regular XNA or C# program, this is where you are going to declare any global variables used by the shader- things like light direction, world, view and projection matrices, light colors, and any other variables your shader might need. These global variables are especially important because these also act as parameters that can be accessed from XNA or C#, acting as a bridge for you to pass data over from XNA into the shader. You call the effect.Parameters.SetValue() to set the value of a global variable. Here's a list of all the different intrinsic variable types you can use for your global variables.
Vertex Shader Input and Output Structs These were probably the most confusing parts of the shader (for me anyway!), even though they are really extremely simple and intuitive. These structures define what data needs to be passed IN to the vertex shader (VSInput) and what data needs to be passed OUT of the vertex shader (VSOutput) and in to the pixel shader. You'll end up seeing variables such as Position, Color, Texture Coordinates, listed inside of these structures etc. Look at examples of shaders to see what kind of information is passed between the program and the vertex and pixel shader. The problem I had in understanding these structures is that NOWHERE in my program did I ever actually manually pass in something like that. The magic mystery part is that it's all done under the hood. When you call your Draw() method, it automatically passes in your vertex data and stuffs it into the VSInput (that name is arbitrary by the way) structure. Just think of it as a way of consolidating all of your arguments for your vertex shader. A vertex shader is just a method, and the structure which defines the vertex shader input arguments in one neat little package. If you look at the argument for the vertex shader, you can see it takes in the VSInput structure. Heck, you could even forget about using structures if you wanted to and list each argument separately. That is a little messier though and not really recommended. Okay, moving on.
Vertex Shader The vertex shader is just a method that handles all the manipulation of your vertices. This method is run inside of your graphics card, however, making it blazingly fast. Here you will handle transformations of your vertices into world space (if you don't know what that is, don't worry! It will be covered in the ambient lighting tutorial), calculate the amount of light that hits the vertex, manipulate the texture coordinates. You then take whatever information you calculated, store it in your VSOutput struct, and return it (Notice the return type for the VertexShader is VSOutput. Just like a method in C# or XNA!). That data will be used further down the pipeline in your pixel shader. Don't worry about telling it where to go though, your graphics card handles all of that. The powerful thing about vertex shaders is that you can really do whatever you want to a vertex. You could add in code that would make the vertex color lighter if it's Y value was higher. You could also even simulate an ocean wave by keeping track of a time variable and using sine functions (that will be a tutorial in the future by the way!). The shader language is very powerful, so use your imagination! After you get some practice under your belt, you'll be able to program whatever you can dream up (until the math makes your brain implode)!
Pixel Shader The pixel shader is a method that handles manipulations of the color of an individual teeny tiny pixel. Based on the information stored in the global variables and the information your vertex shader output, you can do some pretty advanced calculations on each pixel. Just like vertex shaders, under the hood of your beasty graphics card, it runs the pixel shader for each pixel. You're probably confused because you're wondering where it gets the position and other information from. We never specify the position of a pixel in our program or any properties of it! True, but we do for our vertices; models are defined by their set of vertices. So how do you find out the information for a pixel between several vertices? A little thing called interpolation. The pixel shader is pretty smart and knows exactly where the pixel is, how to calculate which way it's facing and all sorts of fun stuff. Once your pixel shader finishes its calculations, it returns a single float4, which stores the alpha, red, green and blue components of the pixel (ARGB). Just think, all this work just to calculate a stinkin' color!
Techniques and Passes A technique is just a simple way of XNA asking "use only these shaders. please" when it draws something. In a technique, you get to define exactly which shaders to use, when to use them, how to layer them on top of eachother, etc. Yes, you heard me right, you can use multiple shaders in one draw call. This is done through the use of passes. A technique is made of several passes. Each pass consists of a vertex shader and a pixel shader. But setting up several passes in a certain order, you can combine a bunch of shaders to create really complex effects. We won't be doing anything along those lines for a while, so you can wipe the sweat of you brow!.
Vertex Shaders vs. Pixel Shaders
For several years, there's been an underground battle between vertex and pixel shaders, but the pixel shaders are quickly gaining the higher ground. What the hell am I talking about? It's best explained by a picture, which I have right below here.
 Now before you get worried about knowing what exactly that picture is, I'm going to tell you not to worry. On the left, we are lighting up a sphere using "per-pixel" lighting. On the right, we have an example of "per-vertex" lighting. Both are pretty intuitive concepts: in the former, the lighting calculations are done on the pixel shader, the later on the vertex shader. As you can see, the pixel shader example looks much better than the vertex shader. This is because for each pixel, we're using it's interpolated position and normal data. Although usually more performance-intensive, most graphics cards these days have plenty of power to handle it. On a vertex shader, it can only determine the color of each vertex. It doesn't know what the colors look like between the vertices, so it just guess. And as anyone can tell you, the right answer is always better than the guessed answer. So if you want smooth lighting, put your calculations on the pixel shader. If your graphics card is just hanging on by a thread, put the lighting calculations in the vertex shader. That might be easier said than done, but don't get too worried; you'll get a hang of this stuff as you make your way through my tutorials.
The Flow of the Shaders... I've provided a "nice" *cough* little picture for you below that gives you an idea of how the whole shader thing works out.
 Image 1 - A crude step-by-step picture of what happens when you want to draw something.
1) You set all the global variables in your shader to whatever values you want. You'll have to pass in your World, View and Projection matrices to pretty much every single shader. You'll also be passing in colors, color intensities, light directions, light intensities, etc. etc. 2) Here's where we dive under the hood of the graphics card and they hide everything from you. When you call DrawPrimitives() or Model.Draw(), it checks to see which technique you selected earlier. Once you're in that technique, it starts going through all of the passes. You can see in my XNA code on the left, I'm iterating through each pass in the current technique (I know this is a lot right now, but don't worry. The ambient lighting shader will give some concrete examples of all of this.) So now that you have a technique and you're on a certain pass... 3) It calls the vertex shader method. In your pass, you specify which vertex shader you want it to use. Now when you call the vertex shader, you're probably asking "Where the heck is it getting the data that needs to be passed to the vertex shader?" That answer I don't exactly know to be honest. Since it's a little over my head, I just picture that when I make my Draw() call, the graphics card now has a theoretical filing cabinet with all of my vertex data. It starts from the top drawer, goes through the files one by one, passing each onto the vertex shader. Then it goes to the next drawer, goes through each file, etc. Once my filing cabinet has successfully been "Vertex-Shaderized" (fancy isn't it?)... 4) It calls the pixel shader method, once again specified in your pass. After each pixel has run through the shader, you now have a single frame from your 3D application.
Conclusion
Well I hope my little introduction to shaders gave you a somewhat basic understanding of how things work and didn't just make you so frustrated that you've smashed your computer and completely given up the technological lifestyle. I didn't cover everything here, but I did that on purpose. There's a lot of technical aspects to shaders that I think are just best left as part of the learning curve, figuring them out as you go along. I hope to provide some those technical details as you travel along that curve. Anyway, if you have any questions at all, feel free to send me an email at dan@digitseven.com. And be sure to head over to the home page and sign up for the newsletter to stay updated when new tutorials and code are released. Good luck and happy programming! Last Update: 8/19/10
| |
|
|