Digit 7

Your source for C# and XNA tutorials and code.

Home     Shader Tutorials     Downloads      
Introduction     Ambient Lighting     Diffuse Lighting Tutorial     Specular Lighting     Texturing Tutorial     Rim Lighting Tutorial     Cel Shader Tutorial     Bump Mapping      
SHADERS - Texturing



A simple box with a stone block texture.

     This tutorial will cover attaching an image (I'll be using the word texture from now on) to a model to give it a much more detailed look. Texturing is an essential of creating a realistic scene because it allows us to give objects very fine detail. Without textures, everything in the scene would basically be one color, look very flat and just be generally boring. The right textures can even give an object the illusion of depth, further increasing the realism. The stone box above looks like it has deep cracks in it between the blocks, but that's just an illusion. The entire box is defined using eight vertices. To geometrically represent those cracks, it would take thousands of vertices, something that definitely isn't feasible.
     So how do we take a texture and apply it to an object? The whole process is quite simple. If you remember back to the diffuse shader tutorial, we started using the
VertexPositionNormalTexture structure for our vertices. At the time I told you to ignore the texture part but now we'll be making full use of this! Following the normal data in the constructor of VertexPositionNormalTexture , it asks for a set of texture coordinates: a Vector2 used to tell how our shaders how the texture is attached to the model. What are texture coordinates you ask?

An Explanation of Texture Coordinates

     Texture coordinates are very simple concept. Take a look at this diagram below; it's a breakdown of the stone box pictured above.



     As I mentioned before, texture coordinates are a Vector2. The horizontal axis of the image is assigned to the U component and the vertical to the V component. Each component represents the proportion of distance from the origin along that axis. In simpler terms, the value of U and V each normally range from 0.0 - 1.0, the origin being the upper-left corner of the image.
     When we specify the texture coordinates for a vertex, we're telling it what part of the texture attaches there. Looking at the example I provided above, we'll take the lower-left triangle on the front face. To attach the texture so it covers the entire face, my texture coordinates would be as follows:
Upper-Left Front Vertex: The origin of the image should attach directly to that point, so (0 , 0).
Bottom-Left Front Vertex: The lower-left corner of the image should attach here, so (0 , 1).

Bottom-Right Front Vertex: Following the same principle as the first two, (1 , 1).
     Just to keep this simple, I'm using one's and zero's. If you wanted to use just the upper-left quadrant of the image, (using the same triangle again) I would use these texture coordinates:
Upper-Left Front Vertex: The origin of the image should attach directly to that point, so (0 , 0).
Bottom-Left Front Vertex: We're only going halway down the V axis this time (0 , .5).
Bottom-Right Front Vertex: The middle of the image should attach here, so (.5 , .5).
Since we're only using half the image now, it would give a stretching effect. Once you get your shader working properly, experiment with adjust your texture coordinates to see what effects different values give.


     The shader will then go through each triangle on the scene, the pixel shader interpolating the texture coordinates between all the vertices of each triangle. That's how you can specify only three attachment points on a triangle, but get the image spread across the entire surface. Once you get the texture coordinates for the pixel, it will sample the texture at those coordinates, returning the pixel color (which you can adjust by the amount of light that hits it, etc.) That's it! Let's take a look at the shader code.



The All-Purpose Shader

    
I've added the texture code to the shader we've been building up over the last few tutorials. If you scan the code, you'll probably notice how little code I added. In fact I only added about 4 lines of code to implement texturing, none of them being math-oriented either (thank the heavens). Now we have a basic shader that can handle ambient, diffuse and specular lighting while also allowing you to add textures to your models.


float4x4 World;
float4x4 View;
float4x4 Projection;

float4 AmbientColor;
float AmbientIntensity;

float4 DiffuseColor;
float DiffuseIntensity;
float3 DiffuseLightDirection;

float4 SpecularColor;
float SpecularIntensity;
float Shinniness;
float3 CameraPosition;

// Texturing variables
Texture ModelTexture;        // Our texture which will be mapped onto our object

// Pass coordinates to our texture sampler to get a color for a certain pixel
sampler TextureSampler = sampler_state {
    texture = <ModelTexture> ;
    magfilter = LINEAR;
    minfilter = LINEAR;
    mipfilter= LINEAR;
    AddressU = WRAP;
    AddressV = WRAP;};


struct VertexShaderInput
{
    float4 Position : POSITION0;
    float3 Normal : NORMAL0;
    float2 TexCoords : TEXCOORD0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float3 Normal : TEXCOORD0;
    float3 CameraView : TEXCOORD1;
    float2 TexCoords :TEXCOORD2;
};

VertexShaderOutput VertexShader( VertexShaderInput input )
{
    VertexShaderOutput output;
   
    float4 worldPosition = mul( input.Position, World );
    float4 viewPosition = mul( worldPosition, View );
    output.Position = mul( viewPosition, Projection );           
       
    output.Normal = mul( input.Normal, World );
    output.CameraView = normalize( CameraPosition - worldPosition );    
   
    // Just pass the texture coordinates to the vertex shader output.
    // When they transfer to the pixel shader, they will be interpolated
    // per pixel.
    output.TexCoords = input.TexCoords;
   
    return output;
}

float4 PixelShader( VertexShaderOutput input ) : COLOR0
{
    // Sample our texture at the specified texture coordinates to get the texture color
    float4 texColor = tex2D( TextureSampler, input.TexCoords );
   
    float3 lightdir = normalize( DiffuseLightDirection );
    float3 norm = normalize( input.Normal );
    float3 halfAngle = normalize( lightdir + input.CameraView );
    float specular = pow( saturate( dot( norm, halfAngle ) ), Shinniness ) * SpecularColor * SpecularIntensity;
   
    float4 diffuse = dot( lightdir, input.Normal ) * DiffuseIntensity * DiffuseColor;
    float4 ambient = AmbientIntensity * AmbientColor;
   
    return texColor * ( diffuse + ambient + specular );
}

technique Texturing
{
    pass Pass0
    {
        VertexShader = compile vs_1_1 VertexShader();
        PixelShader = compile ps_2_0 PixelShader();
    }
}


Changes to The Vertex In/Out Structures

     Nothing major happened here. All we did was add a float2 to each structure to store our texture coordinates. I've just assigned the next TEXCOORDS semantic that was available. The input structure will contain texture coordinates for the vertices and the output structure will contain the interpolated texture coordinates for the pixel shaders, which is what we want.

Two New Variables


// Texturing variables
Texture ModelTexture;        // Our texture which will be mapped onto our object

// Pass coordinates to our texture sampler to get a color for a certain pixel
sampler TextureSampler = sampler_state {
    texture = <ModelTexture> ;
    magfilter = LINEAR;
    minfilter = LINEAR;
    mipfilter= LINEAR;
    AddressU = WRAP;
    AddressV = WRAP;};


1. ModelTexture - This is where the texture is stored. We call SetValue() on the XNA side with a reference to Texture or Texture2D to set the texture we want our shader to sample.
2. TextureSampler - This handy little device allows us to pass in a pair of texture coordinates and get the color of the texture at that point. It may look complicated to declare but it really isn't. There's a whole boatload of arguments )separated by semicolons) you can specify for a sampler but I've just chosen six here. The first is a reference to the texture we want to sample, so we put ModelTexture between <>. The next three are filters applied to the texture. If you want to learn what they do, you'll have to do a little research on your own (Google!). The next two, AddressU and AddressV, have several states:
WRAP - If the values exceed 1, the texture is repeated along that axis. So if AddressU was set to WRAP and the U coordinate was 5, it would repeat 5 times along the U axis.
CLAMP - The values are clamped between 0 and 1. If they precede 0, they are set to 0. If they exceed 1, they are set to 1.
MIRROR -
Similar to WRAP, but each consecutive tile is a mirrored from the last along the axis perpendicular to the mirrored axis.

Vertex Shader

     The only line I added here was passing the vertex shader coordinates to the output structure so they could be interpolated in the pixel shader.

Pixel Shader

     Once in the pixel shader, we can finally sample our texture for what we need. By calling tex2D() on our texture sampler and the interpolated texture coordinates from our input data, we get back a float4 which represents the color of the texture at that pixel. Perfect! Now what to do with it?
     In the previous shaders, everytime I added a new lighting component, I would add it to the total sum. With texture color this is different. The texture color is independent of the light; it is an intrinsic property of the object we are working with. If our box is textured with a stone wall, it's going to be colored like a stone wall if the lights are on or off. By multiplying our texture color by the sum of the lighting components, we can get our desired effect of having a darker color when it's dark and a lighter color when it's bright.

The XNA Side of Things

     There isn't a whole lot to do on the XNA side to use our new shader. Make sure to reference to your new shader and change the technique name to "Texturing" or whatever you chose. Since our shader now needs a texture, you're also going to need to declare a Texture2D variable. In the LoadContent() method, load whatever texture you please into the texture variable from the content pipeline. Then in the main Draw() method of your game, set the value of ModelTexture to the Texture2D variable you declared so the shader knows what texture to draw. You should now have a textured model on the screen!

Conclusion

     Congratulations! If you've followed the tutorials to this point, you've created a shader that basically replicates the functionality of the BasicEffect class in XNA. "What?! They already wrote this stuff?" Easy there. You may have replicated some existing functionality, but you now have a superb understanding of how shaders work ( Hopefully. Or you are just amazingly confused at what I've been talking about this entire time which is a very real possibility) and a fully tweakable shader.
     By adding texturing to our all-purpose shader, you can now give an amazing amount of detail to characters, buildings, cars, trees, weapons or whatever else might be inhabiting your game world. Texturing gives objects the illusion of depth and detail without defining extra geometry. If you continue your shader education, you will find some amazing techniques used to give the illusion of depth and detail such as bump mapping and parallax mapping. And guess what? Both of those techniques use special textures that are sampled in nearly the same way we did with this shader.
     I will be covering more advanced shaders from now on, so make sure you have the details in the tutorials up to this point down pat. Feel free to experiment with the code we've written to get a firmer grasp on what each part of it does. Try to write a shader to give your model a unique effect not covered here. Good luck and happy programming!

DOWNLOAD Texturing PROJECT HERE

Last Updated: 8/19/10