Alright, there's posts flying everywhere about toon shaders, so here's a run down on how one works
.
To begin with, in a shader you need to declare what variables you are going to use, this is done at the top of your shader:
Code:
float4x4 matWorldViewProj;
float4x4 matWorld;
float4 vecSunDir;
Texture entSkin1;
Texture mtlSkin1;
Here you see 3 different variable types, float4x4, float4 and Texture. There are a few others, including float3x3 float3, float2, and float.
Float variables are simply a number or an array of numbers.
Float would be a single number. Float2 would be a 2d coordinate. Float3 would be 3d. And float4 is a 4d coordinate (x,y,z,w). 4D coordinates are used in vector multiplication and other operations. W will equal 0 if it is a vector (direction), and 1 if it is a position when using float4.
We declare the float4, vecSunDir. This is the directional vector of our sun.
float4x4 and float3x3 are matrices. These are used to transform, rotate, and scale vectors and postions. These will usually consist of various predefined ones:
matView
matWorld
matProj (projected)
matWorldView (this is matView*matWorld)
matWorldViewProj (matView*matWorld*matProj)In our declarations we use matWorldViewProj, when you multiply a position in a vertex shader by this, it is tranformed into clip space. This is something that is a good rule of thumb to always do. We will get to this later.
Last, we have Textures. These are simple, they simply define what you are going to use for textures.
The textures can be mtlSkin1..4 and entSkin1..4 for material and entity skins 1-4, respectively.
Now that we have our variables declared, we can go on to create our tecnique.
Code:
technique lightMapping
{
pass SingleLight
{
//globals
zwriteenable= true;
zenable = true;
In a technique, which is the code that tells the shader what it is supposed to do, you can have multiple passes. A
pass is simply one time rendering the model. In this part we are only going to do the lighting pass, for a toon shader you can add a second pass that will render the model slightly larger, flip its normals, and make it black- this creates a ink outline. Everything inside this technique is considered the Fixed Function Pipeline, or FFP. You can call vertex and pixel shaders from inside of this.
Our global variables, zwriteenable and zenable, simply enable Z buffering for our render and disallow the shader from rendering new depth values.
Each pass is made up of
stages. Stages are individual layers of texture that are blended together in different ways to create the texture appearence of the model. In our first stage we will render our model without any lighting, and in the second we will multiply the lighting over it (lighting of 1 keeps the texture fullbright, where 0 makes it black).
Code:
//Stage variables
Texture[0]=<mtlSkin1>;
ColorOp[0]=Modulate;
ColorArg1[0]=Texture;
ColorArg2[0]=Texture;
AddressU[0]=Clamp;
AddressV[0]=Clamp;
AddressW[0]=Clamp;
We are setting several stage variables here. Stage variables (such as Texture) are followed by brackets [] that enclose the stage number you are setting the variable for. We are going to use the material skin for the first pass, which will contain an image holding lighting values from black to white going from left to right in the image. To do this, we enclose the texture in angular brackets <mtlSkin1>. This is how we reference declared variables in the FFP. Next, ColorOp defines how the texture should be rendered atop other stages. Modulate makes it multiply it over whatever is already there. When doing it in the first stage, it simply sets the texture as what you are giving it. Our two colorArg variables determine how the texture itself is rendered. You can use Texture and Diffuse for example to get simple diffuse lighting. Here we want a fullbright (no shadows/lighting) texture, so we give it Texture and Texture. Lastly, we want to clamp the texture at the edges so it doesnt tile. We do this by clamping the AddressU..W variables.
Next we need to render our model texture. We will render this in the second stage, multiplied over the lighting stage to achieve the final effect.
Code:
Texture[1]=<entSkin1>;
ColorOp[1]=Modulate;
ColorArg1[1]=Texture;
ColorArg2[1]=Current;
The only thing new here is that we use Current for the second colorArg. This just takes what was previously defined. This isn't required, but good for conisistencey.
Lastly, we need to call our vertex shader (that we will define). This is done by setting VertexShader. When using HLSL, we don't put the code in the FFP, but rather define functions for it. So we
compile for
vs_1_0 (vertexshader 1.0), our function
VS_Main().
Code:
//shaders
VertexShader = compile vs_1_0 VS_Main();
}
}
Now, we are ready to code our vertex shader. Let's go inbetween our declarations and our technique to declare this.
Before we start, we need to do something similar to our declarations- and that is to define our input and output structures for the vertex shader.
Code:
struct VS_INPUT
{
float4 position : POSITION;
float2 TexCoords: TEXCOORD0;
float3 normal : NORMAL;
};
struct VS_OUTPUT
{
float4 position : POSITION;
float2 uvCoords: TEXCOORD0;
float2 TexCoords: TEXCOORD1;
float4 diffuse : COLOR;
};
I'm not going to go into the syntax too heavily, just notice the format:
Code:
struct STRUCT_NAME
{
varType varName : VARIABLE;
};//don't forget the ';' !!
We want the engine to give us the vertex position, first texture coordinate, and the lighting normal, so we use POSITION, TEXCOOD0, and NORMAL. We cannot change those, but we can change what we call them in our code: position, TexCoords, and normal (note that these are case sensitive). We are required to take the POSITION and return it. So, in our output, we return out the POSITION along with two texture coordinates, corresponding to the first two texture stages in our FFP. Also, we want to return the diffuse color for lighting. Some cards have defaults set in the diffuse, which causes artifacts- so we will set diffuse to be a static value in our shader.
Now lets code the shader!
Code:
VS_OUTPUT VS_Main(VS_INPUT input)
{
//zero out the struct members
VS_OUTPUT output = (VS_OUTPUT)0;
//our code will go in here
return output;
}
Notice the structure again being:
Code:
OUTPUT_STRUCT Function_Name( INPUT_STRUCT inputName ) //structName is what we will call it in our code
{
OUTPUT_STRUCT outputName = (OUTPUT_STRUCT)0; //this defines our output variable, using our own variable type (just like we would define with float for eample). We also have to "zero it out", done with "= (OUTPUT_STRUCT)0".
return outputName; //return our output structure variable to the shader- this will either be passed directly to the FFP, or to a pixel shader if one is used.
}
//note: no ';' here, only on structs.
Now that we have the structure, the first thing we will do is, as I mentioned earlier, transform our vertex position into clip space.
Code:
//transform vertex pos to clip space
output.position = mul(input.position, matWorldViewProj );
We reference variables inside of our structures like skills in C-Script:
struct.var. Here we are working with a float3, but we don't need any fancy vec_set or anything here. HLSL will assume what it is supposed to do. the
mul function will multiply the first argument by the second. So here, we are multiplying the position "into" the clip space by using matWorldViewProj. This takes a bit of 3d math theory, so look it up if you don't quite understand it.
Next, we need to work with our texture coordinates. The second set (TexCoords) we aren't going to do anything with, so lets do those first just copy the texture coordinates that we already have back into it to be returned:
Code:
output.TexCoords= input.TexCoords;
Next, we need to calculate our light direction:
Code:
float4 LightDir = mul(matWorld,-vecSunDir);
Here we are multiplying the world matrix by the sun direction (inverted). This gives us something to work with when we calculate where the texture coordinate to use for our lighting should be (remember it is reading the lighting values from a bmap, we need to know where they should be read from).
Now, we can calculate that position:
Code:
float diffuse = max(0,dot(LightDir,input.normal ));
diffuse is where we will store the light intensity. We make sure it doesn't go below zero- if this happens the lighting will wrap around to the other side of the object. We use a simple max() instruction to do this (takes larger value, so if the second argument is below 0, 0 will be used.) We use another new function here,
dot. This computes the dot product of two vectors. By calculating the light direction to to normal, we get the light intensity (look at some light thoery tutorials if you need more explanation).
Now, we are ready to set those lightingmap texture coordinates!
Code:
output.uvCoords.x = diffuse;
output.uvCoords.y = 0.0f;
We are reading it horizontally, so we set the X coordinate based on the light intensity, and the Y coordinate to 0. The Y coordinate doesn't matter very much because we will use a single pixel height image.
And now? We're done! The next line of code we should have after this is the "return output;" that we put in our structure.
Here is an example image to use for the lighting image:
TGA Image This shader requires VS1.0 and NO PS!
If you use the code put in here, a note of credit would be nice.
-Rhuarc