Wednesday, April 23, 2014

OpenTK Tutorial 6 Part 2: Loading Textures and the TexturedCube Class

Now that we have our shaders loaded in a new, more object-oriented way, we're a third of the way to having textured objects in our program!

In this tutorial, we'll add a function to send textures to the graphics card and write a new class to give us a cube with texture coordinates. Unfortunately, we'll need to wait for part 3 to put these to use, but I'll try to hurry it along (summer is almost here)!


To load an image, we'll need to make a new function in the Game class. This function will load an image as a bitmap, and then store the data of it in the graphics card's memory. It returns an int, the address of the texture so that we can retrieve the data when we want to draw the texture.

        int loadImage(Bitmap image)
        {
            int texID = GL.GenTexture();

            GL.BindTexture(TextureTarget.Texture2D, texID);
            BitmapData data = image.LockBits(new System.Drawing.Rectangle(0, 0, image.Width, image.Height),
                ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);

            GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, data.Width, data.Height, 0,
                OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, data.Scan0);

            image.UnlockBits(data);

            GL.GenerateMipmap(GenerateMipmapTarget.Texture2D);

            return texID;
        }

Let's dissect this function. First it has OpenGL generate a texture ID for us to work with. Then we bind it as a 2-dimensional texture (since it's an image) and send the data for its pixels to the graphics card. Finally, it tells the graphics card to make mipmaps for the texture.

Mipmaps are scaled down versions of a texture to improve performance and prevent aliasing. When a texture is being drawn at a smaller size, the graphics card will use a smaller mipmap texture to lighten its load.

 
Mipmaps of a picture of a very old picture of the ISS, from Wikipedia


While we're here, let's make an overload to make things easier for us in the future:

        int loadImage(string filename)
        {
            try
            {
                Bitmap file = new Bitmap(filename);
                return loadImage(file);
            }
            catch (FileNotFoundException e)
            {
                return -1;
            }
        }

This just allows us to give the program a filename to load from.

Now that we can put a texture into memory, let's set things up so we have an easy way to keep track of our texture IDs. Just like we had a Dictionary to allow us to use strings to organize our shaders, we'll use another to keep track of our texture IDs. Add this to the Game class:


        Dictionary<string, int> textures = new Dictionary<string, int>();

Next we'll set up the other class we're going to need, the TexturedCube class (add a new file for it).

First, we need to add some variables (and an abstract function) to the Volume class to store information about textured objects:

        public bool IsTextured = false;
        public int TextureID;
        public int TextureCoordsCount;
        public abstract Vector2[] GetTextureCoords();

These variables let us store if a Volume object has a texture assigned to it, what that texture is, and how to draw it.

Before we continue, we'll have to implement GetTextureCoords in the Cube class (along with any other subclasses of Volume you may have implemented). We'll just return an empty array of Vector2 objects, since our plain ol' Cube class doesn't have any texture. Add the following to the Cube class:

        public override Vector2[] GetTextureCoords()
        {
            return new Vector2[]{};
        }


Now, let's write the TexturedCube class, to use these new additions:

    class TexturedCube : Cube
    {
        public TexturedCube()
            : base()
        {
            VertCount = 24;
            IndiceCount = 36;
            TextureCoordsCount = 24;
        }

        public override Vector3[] GetVerts()
        {
            return new Vector3[] {
                //left
                new Vector3(-0.5f, -0.5f,  -0.5f),
                new Vector3(0.5f, 0.5f,  -0.5f),
                new Vector3(0.5f, -0.5f,  -0.5f),
                new Vector3(-0.5f, 0.5f,  -0.5f),

                //back
                new Vector3(0.5f, -0.5f,  -0.5f),
                new Vector3(0.5f, 0.5f,  -0.5f),
                new Vector3(0.5f, 0.5f,  0.5f),
                new Vector3(0.5f, -0.5f,  0.5f),

                //right
                new Vector3(-0.5f, -0.5f,  0.5f),
                new Vector3(0.5f, -0.5f,  0.5f),
                new Vector3(0.5f, 0.5f,  0.5f),
                new Vector3(-0.5f, 0.5f,  0.5f),

                //top
                new Vector3(0.5f, 0.5f,  -0.5f),
                new Vector3(-0.5f, 0.5f,  -0.5f),
                new Vector3(0.5f, 0.5f,  0.5f),
                new Vector3(-0.5f, 0.5f,  0.5f),

                //front
                new Vector3(-0.5f, -0.5f,  -0.5f), 
                new Vector3(-0.5f, 0.5f,  0.5f), 
                new Vector3(-0.5f, 0.5f,  -0.5f),
                new Vector3(-0.5f, -0.5f,  0.5f),

                //bottom
                new Vector3(-0.5f, -0.5f,  -0.5f), 
                new Vector3(0.5f, -0.5f,  -0.5f),
                new Vector3(0.5f, -0.5f,  0.5f),
                new Vector3(-0.5f, -0.5f,  0.5f)

            };
        }

        public override int[] GetIndices(int offset = 0)
        {
            int[] inds = new int[] {
                //left
                0,1,2,0,3,1,

                //back
                4,5,6,4,6,7,

                //right
                8,9,10,8,10,11,

                //top
                13,14,12,13,15,14,

                //front
                16,17,18,16,19,17,

                //bottom 
                20,21,22,20,22,23
            };

            if (offset != 0)
            {
                for (int i = 0; i < inds.Length; i++)
                {
                    inds[i] += offset;
                }
            }

            return inds;
        }

        public override Vector2[] GetTextureCoords()
        {
            return new Vector2[] {
                // left
                new Vector2(0.0f, 0.0f),
                new Vector2(-1.0f, 1.0f),
                new Vector2(-1.0f, 0.0f),
                new Vector2(0.0f, 1.0f),
 
                // back
                new Vector2(0.0f, 0.0f),
                new Vector2(0.0f, 1.0f),
                new Vector2(-1.0f, 1.0f),
                new Vector2(-1.0f, 0.0f),
 
                // right
                new Vector2(-1.0f, 0.0f),
                new Vector2(0.0f, 0.0f),
                new Vector2(0.0f, 1.0f),
                new Vector2(-1.0f, 1.0f),
 
                // top
                new Vector2(0.0f, 0.0f),
                new Vector2(0.0f, 1.0f),
                new Vector2(-1.0f, 0.0f),
                new Vector2(-1.0f, 1.0f),
 
                // front
                new Vector2(0.0f, 0.0f),
                new Vector2(1.0f, 1.0f),
                new Vector2(0.0f, 1.0f),
                new Vector2(1.0f, 0.0f),
 
                // bottom
                new Vector2(0.0f, 0.0f),
                new Vector2(0.0f, 1.0f),
                new Vector2(-1.0f, 1.0f),
                new Vector2(-1.0f, 0.0f)
            };
        }
    }


I know this seems like a bit of a code dump, but it's mostly just new data for the class to return instead of the Cube's, so there's not much to talk about.

 The texture coordinates are the only truly new part of this class. These coordinates relate vertices of our cube to points on our texture (if you do 3D modeling, it's just like UV mapping). Like other shader attributes, the texture coordinates are interpolated between points to make a smooth transition from one value to the next. This means that for our cube, we only need to define the texture coordinates at the corners, and the graphics card will stretch our texture to match what we input.


For this example, I've just made it so every side has the same part of the texture drawn on it. Later, we'll import models that have complex texture mappings that really show how textures can be used.
____

In part 3, we'll actually put this code to use. I broke it off here because I prefer to leave each part of a larger tutorial off on usable code.

14 comments:

  1. Wow I've been following your tutorials from the first one and I have to say that they are excelent!

    I have a question and I hope you can help me with it.
    I want to rotate one of the cubes I have from a different axis, not from its center... how can I achieve that ?

    Thanks for the tutorials

    ReplyDelete
    Replies
    1. Hi Felipe,

      It's all in the order of operations. When you rotate it, it's always around the origin. So, first translate it so that it's offset so it's relative position to the origin is the same as the relative distance to the axis will be. Then do your rotation and translate it to where you want it.

      Delete
    2. This comment has been removed by the author.

      Delete
  2. Straightforward steps as usual. Great stuff!

    Pete

    ReplyDelete
  3. I was following your tutorials and in these past few tutorials, I came across a problem where whenever I try and run the engine, and a GL.DrawElements is called, it throws an error saying the data is corrupted. Any help?

    ReplyDelete
    Replies
    1. Hi Joey,

      Can you please post your Game.cs code to somewhere like pastebin? I'll have to take a look at it to find out what's happening.

      Delete
    2. http://pastebin.com/nDaEcTEa

      As you can see, I did not follow your tutorials exactly, I used some of my own dictionaries and interfaces to make things easier to organize later. You can also ignore the OBJMesh stuff, I had been toying around with loading in graphics models as well. Thanks!

      Delete
    3. Also, the actual GL.DrawElements invokes have been commented out just so I could get the engine to compile and run, but obviously the error only occurs when it is not commented out.

      Delete
    4. Hi Joey,

      It looks like you're missing the calls to BindBuffer and BufferData for the element array buffer in OnUpdateFrame. Please tell me if this doesn't fix the issue.

      By the way, thanks for sharing your code, I liked how you set up your classes. You might see some resemblance to it later on.

      Delete
    5. Right after my GL.UseProgram in the OnUpdateFrame method I have these 2 lines from the previous tutorial:

      GL.BindBuffer(BufferTarget.ArrayBuffer, shaders[activeShader].GetBuffer("vPosition"));

      GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(vertdata.Length * Vector3.SizeInBytes), vertdata, BufferUsageHint.StaticDraw);

      Perhaps I should have them before my model matrices are calculated? As a side note, the Update method is invoked before the render method is right?

      Also, thanks for the compliment, I'd love to see some familiar setup in future tutorials.

      Delete
    6. School just got out and I've had a chance to give a look at my code again; it seems to me like there is something wrong with the shaders or at least the way I implemented them or something. Although there are no errors regarding attribute binding, I think that could be possibly be the cause.

      Delete
  4. Dying to see the next tutorial, I have tried setting the texture to our new TexturedCube but I'm getting errors all over it. Keep up the good work Kabuto.

    ReplyDelete
  5. Hi Kabuto,
    I would like to ask you for a small kidness. Please, show me how to use OpenTK
    in UserControl or in System::Windows::Forms::Panel.
    Thanks

    ReplyDelete
  6. Just a thanks for the tutes. Moving away from XNA to OpenGL and I've followed along well so far. Eagerly awaiting the final part for textures so I can run into the hills gleefully with textured crates everywhere.

    Got a recommendation on where to head to get more useful shaders? XNA by default runs what looks like a phong shader.

    ReplyDelete