Sunday, April 24, 2011

Simple texturing

To increase the realism of our scene we can add textures to our 3D models. Textures are a great and simple way to increase the detail of our game, they are used for a lot of different effects like normal mapping. But let's not get ahead of ourselves and just see how we can render a simple textured cube:


Backreferences:
Remember that backrefences are recursive!

The source from where this tutorial begins is here. It has a few things already implemented, a vertex buffer with position texture and normal information (we will not use the normal in this tutorial but it will come in handy later). It also has a index buffer and an effect set up to draw pure black, this way you don't have to redo everything from the former tutorial.

To get from that 'shadow' of a cube to an actual textured cube we have to:
  1. Add texture coordinates to the shaders input and output.
  2. Add a texture and a sampler to our shader
  3. Sample the texture in the pixel shader
  4. Load the texture and set it to the effect
  5. Sampler options (optional but recommended)
1. Add texture coordinates to the shaders input and output.

You can do this by just adding a line to the vertex shader input and output structures like so:

struct VertexShaderInput
{
    float4 Position : POSITION0; 
    float2 Texture  : TEXCOORD0;
};
struct VertexShaderOutput
{
    float4 Position : POSITION0; 
    float2 Texture  : TEXCOORD0;
};

The texture itself is a flat surface so only 2 coordinates are enough,we also use TEXCOORD0 semantic as we have only 1 texture.
Now our vertex shader must pass the texture coordinate to the pixel shader, we do this by adding this line to the vertex shader:

output.Texture = input.Texture;

The only thing this little line of code does is to copy the texture coordinate from input to output.

2. Add a texture and a sampler to our shader

 The texture type we are going to use is texture2D so we will have to add this little line to our code:

texture2D tex;

 Now that we have a 2D texture parameter we will also need a sampler. Whenever you want to read something from a texture on the GPU you will use a texture sampler. Sampling a texture on a GPU is extremely fast and efficient so you can even use them as lookup tables for complex equations. Our sampler only has to know what texture it will be connected to:

sampler2D texSampler = sampler_state
{
    Texture = <tex>;
};

The syntax is very simple, just sampler2D SAMPLERNAME = sampler_state{parameters};. At the moment the only parameter we need is the name of the texture.

3. Sample the texture in the pixel shader

Now that we have our sampler state set up we can use it to texture our cube. We will use the intrinsic function tex2D. This function returns the color of the pixel form a texture, it has 2 input parameters, a sampler state and the coordinates to sample:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    return tex2D(texSampler,input.Texture);
}

Remember that the output from the vertex shader is interpolated before reaching the pixel shader, so each pixel has it's own texture coordinates.

4. Load the texture and set it to the effect

Now that our effect is finished we only have to add the texture. Right click the content project Add->Existing Item and add crate.jpg (it should be in the same archive as the source code). Now create a texture object at class level:

    Texture2D crateTexture;

Now go in the LoadContent function and load the image into memory:

    crateTexture = Content.Load<Texture2D>("crate");

Remember between <> you have the type of the resource you are loading and the string must be the asset name of the file.
Last thing left to do is set the parameter of our effect:

    simpleTextureEffect.Parameters["tex"].SetValue(crateTexture);

This line of code should be in the LoadContent method of our game class. Now that we have set everything just run the sample and it should work just fine, but you will notice some artifacts on the sides of the cube. These are caused by texture filtering and can be solved very easily.

5. Sampler options (optional but recommended)

Texture coordinates are considered to be in UVW space, in the case of 2D textures just UV. In this type of coordinates values range from 0 to 1. The sampler has 2 states called AddressU and AddressV we use to tell the graphics card what to do with coordinates outside of this range. To make the effects more visible multiply by 3 the texture coordinate in the pixel shader (you can multiply any vector by a scalar in HLSL). This way our coordinates will be in the 0 - 3 range. You will see that the image is repeated across the surface. That is the equivalent of setting the AddressU and V states to WRAP. If you don't want to repeat the texture you can always use CLAMP, this will just extend the last pixel of our texture. Another interesting address mode is MIRROR, you can ques what this does, right?

Now about those artifacts, they really make the texture look ugly, so what do we do with them? Well we have some texturing filters. These are MipFilter, MagFilter and MipFilter that we can set to tell the graphics card the filtering mode to use for minification filter, magnification filter and mipmap filter. The worst (but fatest) is to use POINT. This will make the sampler return the closest pixel in the texture to our coordinates (as we can have coordinates between pixels). A little better is to use LINEAR, this way the graphics card will make an average between the surrounding pixels of our coordinates. But when the texture is at an angle close to the screen it will still look blurry. To make the image more clean we can set the filtering mode to ANISOTROPIC. This type of filter takes into account how elongated pixels are on the screen (the angle between the texture and view direction). If you are going to use anisotropic filtering you should also set the MaxAnisotropy state to a number between 1 and 16, this will set the quality of our filtering.

The finished project is here, and it should look something like this:

10 comments:

  1. in the LoadContent function you reference
    "crateTexture = Content.Load("crate");"
    Am I correct in assuming this is a standard XNA texture, and if so is there a reference anywhere to check out which default textures are included?

    I've only started playing with XNA and am far more used to loading textures from assets I've either created or downloaded, a look at what's already there to play with would save me a lot of time for simple prototyping!

    ReplyDelete
  2. Hi,

    Yes, it is a standard Texture2D xna textures as declared just 2 lines above, for a reference use msdn http://msdn.microsoft.com/en-us/library/bb197344.aspx. You have to scroll down to get to textures (it's faster to just search for "texture") on the page.

    A simple way to see what type of files can be used as textures is to simply right click on a content project and then select add existing item, and set the filter to texture, this will actually show you the allowed extensions.

    You just started XNA? Welcome to XNA Game Development and have fun!

    ReplyDelete
  3. Your tutorials are great for starting developing in 3D-Space!

    As I come from plain 2D-Stuff this tutorials are worth gold :)
    Thank you very much for your time and effort.

    ReplyDelete
  4. But when if i want to draw multiple cubes? Things like:

    CreateCubeVertexBuffer(0);
    CreateCubeIndexBuffer();
    GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, number_of_vertices, 0, number_of_indices / 3);

    CreateCubeVertexBuffer(1);
    CreateCubeIndexBuffer();
    GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, number_of_vertices, 0, number_of_indices / 3);

    doesnt work... Do I rly need to make new VertexBuffer etc. fo every cube? It would be big performace drop in my app...

    ReplyDelete
    Replies
    1. No,

      First of all you should not create the cube (or any model) when you draw, you should create it once at the beginning.

      Then if you want to use a different model (if you have 2 cubes you can use the same model) just change the current vertex buffer.

      If you want to draw more than one cube, change the World Matrix and then call the draw function again. That's the matrix that tells the cube where it is in the world, and what rotation it has. You can also scale it, just use the Matrix.CreateScale, Matrix.CreateRotationX/Y/Z and Matrix.CreateTranslation (multiply them) to move your object around.

      If you want to see a very simple example just look at my stencil Mirror tutorial. I have to draw the cube twice there, once for the direct view and another time for the reflected view in the mirror. http://iloveshaders.blogspot.ro/2011/05/using-stencil-buffer-rendering-stencil.html

      Delete
    2. Thx for answer, very fast btw. Actually English is not my first language, and I didnt know, that Matrix in XNA is the same what it is in Math... Unfortunelly Im in High School, so I dont know anything about Matrices :( So firstly I have to play with them around, read, do some excercises, and then back to 3D game development.
      BTW. reading some Java 3D games code I havent seen any Matrices (thats why I'm so suprised now). They where using OpenGL. Arent there any matrices?

      Delete
    3. As far as I know OpenGL uses matrices too (for example the glTranslatef() method). But that doesn't mean you have to see them, it depends on the tutorial, maybe they just didn't put them in a very obvious place, or not using them right there.

      Actually, English is not my first language either. :)

      Delete
  5. another tutorial where you kinda not tell HALF the stuff you're doing..... ending up in comparing my own files to your finished ones to find half a dozen lines you didn't even mention you changed in the tutorial.... kinda annoying.

    (it would have been nice if you mentioned that you added some lines in the backreference :( like uhm ... the texcoord's ;) )

    Overall the articles are nice, and a lot of information you explain well, but overall , it's kinda sloppy (luckily you add the final source so the missing part are still findable, but it doesn't help the learning process)

    ReplyDelete
    Replies
    1. That's because you missed the link at the top, that's the code you are supposed to start from, it's not the same code as the one from the previous tutorial.

      Each of my tutorials explains 1 and only 1 concept, to be able to do this without writing a lot of code that's not related to that concept in the tutorial, all the code that's not part of it is in the backreferences file. That means that whatever is written in that file you should be able to understand before starting this tutorial. For example I showed in the precious tutorial HOW TO add information to the vertex data, so that goes in the backrefences and concentrate on how to use them in this tutorial.

      The whole idea is that you already have all that extra code written by me, so you don't have to and can just concentrate on the tutorial at hand.

      Delete