Monday, July 22, 2013

OpenTK Tutorial 3 - Enter the Third Dimension

Now that we've draw one triangle the right way, let's add some real depth to our image. Here we'll be drawing a cube and rotating it so we can see our work from a different angle.

Let's start with what we had at the end of the previous tutorial. It draws a triangle to the screen using a vertex array (instead of using the outdated immediate mode). We set up two shaders, so we can output colors and vertices fairly easily, too.

To draw a cube, we need to figure out how to express it as a sequence of vertices. We're drawing it from a set of triangles, so let's look at how a square is broken up:
If we apply this to each side of a cube, we get something like this:
We have to figure out how to recreate this in OpenTK. One way would be to give it all the vertices for each triangle, which comes to 36 vertices (which is 108 floats for a simple cube!). However, we can accomplish this with a lot less work, only giving it 8 vertices. We'll figure out all the points we want to draw first, and then we can give OpenTK a list of vertices to draw in order, and it'll send them off to the graphics card with less work for us.

This list of vertices (indices, actually) will need its own array for storage. Each indice is an int that acts as a pointer to a vertex we've already defined.

Add this code to the Game class, right after the other arrays are defined:

        int[] indicedata;


Now, we'll need to add the vertices. Replace the existing vertdata code in OnLoad with this:

       

            vertdata = new Vector3[] { new Vector3(-0.8f, -0.8f,  -0.8f),
                new Vector3(0.8f, -0.8f,  -0.8f),
                new Vector3(0.8f, 0.8f,  -0.8f),
                new Vector3(-0.8f, 0.8f,  -0.8f),
                new Vector3(-0.8f, -0.8f,  0.8f),
                new Vector3(0.8f, -0.8f,  0.8f),
                new Vector3(0.8f, 0.8f,  0.8f),
                new Vector3(-0.8f, 0.8f,  0.8f),
            };


We'll also want to add the following to OnLoad:

            indicedata = new int[]{
                //front
                0, 7, 3,
                0, 4, 7,
                //back
                1, 2, 6,
                6, 5, 1,
                //left
                0, 2, 1,
                0, 3, 2,
                //right
                4, 5, 6,
                6, 7, 4,
                //top
                2, 3, 6,
                6, 3, 7,
                //bottom
                0, 1, 5,
                0, 5, 4
            };


This creates a list of vertices to use for each side. Each group of three defines a triangle using the vertices we already have defined.

In order to draw with this new data, we have to change a few more things.

First, we'll need to get another buffer object to put our indice data into. Where we defined vbo_position and vbo_color, add:

        int ibo_elements;


Now, in initProgram, we'll need to add in the following code after where we generate buffers for position, color, and view:

            GL.GenBuffers(1, out ibo_elements);


We're closer, but we still have to write to the buffer and use it to draw. In OnUpdateFrame, add:

            GL.BindBuffer(BufferTarget.ElementArrayBuffer, ibo_elements);
            GL.BufferData(BufferTarget.ElementArrayBuffer, (IntPtr)(indicedata.Length * sizeof(int)), indicedata, BufferUsageHint.StaticDraw);

This sends the array we stored our indices in to the graphics card.

There's one last step we need to do to draw the cube. In OnRenderFrame, we need to replace our call to GL.DrawArrays with:

            GL.DrawElements(BeginMode.Triangles, indicedata.Length, DrawElementsType.UnsignedInt, 0);

If you run the program now, you'll see a rainbow rectangle:

The rest of the cube is there, but we have no way to see it. The front of the cube is facing us, so it blocks out the other sides. To see the other sides, we either need to move it or rotate it. We're going to make it spin, although we'll need to move it too (since otherwise it'll have holes in it where it goes behind the camera).

In OnUpdateFrame, before we call GL.UniformMatrix4 to send our modelview matrix to the graphics card, add this code:

            mviewdata[0] = Matrix4.CreateRotationY(0.02f) * Matrix4.CreateRotationX(0.015f) * Matrix4.CreateTranslation(0.0f, 0.0f, -3.0f) * Matrix4.CreatePerspectiveFieldOfView(1.3f, ClientSize.Width / (float)ClientSize.Height, 1.0f, 40.0f);

A note about this code: here we have two different things being done at once that should really be separated. Right now it's not that important, but a later tutorial will include making the perspective and view matrices separate from the model matrix (when we have more than one model, we should reuse the other two). Also, if you've used OpenGL in the past, you'll note the order of operations being off here, since in a shader you would have perspective before transformations. OpenTK does it different (matrices are transposed compared to OpenGL), requiring us to switch things around.

If you run it now, you'll see that things are a little different:

Now it looks more like a square (it's rotated, so it's not perfect from this viewpoint), but it turned black. We just forgot to add more colors for the other vertices. Replace the coldata part of OnLoad with the following:

coldata = new Vector3[] { new Vector3(1f, 0f, 0f),
                new Vector3( 0f, 0f, 1f), 
                new Vector3( 0f,  1f, 0f),new Vector3(1f, 0f, 0f),
                new Vector3( 0f, 0f, 1f), 
                new Vector3( 0f,  1f, 0f),new Vector3(1f, 0f, 0f),
                new Vector3( 0f, 0f, 1f)}; 

Now, let's get it spinning! First we need a variable to keep track of time. Add the following to the Game class:

        float time = 0.0f;

In OnUpdateFrame, we need to add our delta (time since last update) to this variable. OpenTK gives us this through the FrameEventArgs object the event gets passed. Add this code to the beginning of OnUpdateFrame:

            time += (float)e.Time;

Now it keeps track of time, but we need to do something with it. Replace the line where we set mviewdata[0] in OnUpdateFrame with:

            mviewdata[0] = Matrix4.CreateRotationY(0.55f * time) * Matrix4.CreateRotationX(0.15f * time) * Matrix4.CreateTranslation(0.0f, 0.0f, -3.0f) * Matrix4.CreatePerspectiveFieldOfView(1.3f, ClientSize.Width / (float)ClientSize.Height, 1.0f, 40.0f);

Run it and watch the cube!

You might notice the edges of the cube are a little jagged when it's rotated. We can fix that by making a new constructor for the Game class that tells it to use multi-sampling:

public Game() : base(512,512, new GraphicsMode(32,24,0,4))
        {

        }

This code tells it to create a window 512 by 512 pixels, with 32-bit color depth, 24 bits in the depth buffer, no stencil buffer, and 4x sampling for anti-aliasing, assuming your computer supports it. With this setting, things look a little bit nicer:


20 comments:

  1. I'm loving these tutorials, they're very helpful. Hope you continue them.

    ReplyDelete
  2. Ditto! I'm super confused as to how to add a second cube!

    ReplyDelete
    Replies
    1. Not sure if Anonymous comments receive email replies, but there is a new one up that shows exactly that.

      Delete
  3. I've been looking for a guide like this for quite some time and this is really good. Keep them coming :)

    ReplyDelete
  4. very helpfull, but how to do that each side has own color?

    ReplyDelete
    Replies
    1. You'll need to define the shape differently. Here we simplified it to 8 vertices, but then we'll only have 8 color values as well. So, you'll need to have 4 vertices for each side, with the color you want for that side assigned to them.

      Delete
    2. Could you please describe this more detailed please? I assume that I need to modify coldata variable, but I'm not sure how exactly.

      Delete
    3. You'll need to modify all of the variables. I'd recommend first setting things up like the later tutorial (where we have an object-oriented setup). You will still provide one vertex for each corner of each face (instead of the whole cube, which we did for optimization when color wasn't important), but we'll need to repeat it with four separate vertices for each face (so 24 vertices total). You'll need to provide different color values (24 of them, one to match each vertex), set up with all four of the vertices for one face assigned the same color. Finally, you'll need to change the indices to properly draw both triangles for each face, using the verts made specifically for that face.

      Delete
  5. Just want to give you kudos for making great tutorials! I've been biding my time hoping some good new would surface about XNA but no beans. Monogame looks great but OpenTK gives me the "ground up" control I like. I'm planning to focus my development on Windows desktop, Android and Linux, maybe SteamOS. I'll be following your blog from now on. Thanks! =o)

    ReplyDelete
  6. Great Tutorials, but I have a bit of a problem. My Vertices don't seem to be rendering depth properly, even if I use the new constructor. Did I miss something?

    ReplyDelete
    Replies
    1. Hi,

      Can you go into more detail about your issue?

      Delete
  7. Once again another great tutorial. Thanks!

    ReplyDelete
  8. I have been receiving the following error with the OpenTK projects:

    "A first chance exception of type 'System.DllNotFoundException' occurred in OpenTK.dll"

    It doesn't seem to affect the apps - all the tutorials have been working just fine. Anybody else run into this?

    ReplyDelete
  9. hey i am getting this error its really annoying and it goes like this.





    Unhandled Exception: System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
    at OpenTK.Graphics.OpenGL.GL.DrawElements(BeginMode mode, Int32 count, DrawElementsType type, Int32 indices)
    at OpenTKTutorial2.Game.OnRenderFrame(FrameEventArgs e) in E:\MicrosoftJumpStartC#\OpenTK\OpenTKTutorialContent-master\OpenTKTutorial2\OpenTKTutorial2\Game.cs:line 203
    at OpenTK.GameWindow.RaiseRenderFrame(Double elapsed, Double& timestamp)
    at OpenTK.GameWindow.DispatchUpdateAndRenderFrame(Object sender, EventArgs e)
    at OpenTK.GameWindow.Run(Double updates_per_second, Double frames_per_second)
    at OpenTKTutorial2.Program.Main(String[] args) in E:\MicrosoftJumpStartC#\OpenTK\OpenTKTutorialContent-master\OpenTKTutorial2\OpenTKTutorial2\Program.cs:line 15
    Press any key to continue . . .



    and when i remove the Line of code which is GL.DrawElements(BeginMode.Triangles, indicedata.Length, DrawElementsType.UnsignedInt, 0);

    it runs but without the cube and its really annoying...

    (PS, this is the guy who DMed to on the reddit page about C# with openGL stuff with openTK)

    ReplyDelete
    Replies
    1. Can you post your Game.cs file? This might mean there's a missing line in it.

      Delete
    2. (by the way, make sure to put it on PasteBin or something similar)

      Delete
    3. Oh btw i am using your OpenTK tutorials on Github for reference and already made like i used tutorail 2 for 3 because tutrial 2 carries over to tutrial 2
      heres the the pastbin link

      http://pastebin.com/DRU0i0wS

      Delete
    4. In OnUpdateFrame, on line 224 (should be a call to GL.BindBuffer), replace BufferTarget.ArrayBuffer with BufferTarget.ElementArrayBuffer. That made it stop throwing the exception for me, getting just about the output of the screenshot in the middle of the tutorial (so it looks like the rest is good!).

      Delete
  10. Hi dear Kabuto
    I really confused. please let me have your overall code in C#
    maybe I can learn more if I play with code and run it in sharpdevelop.

    Best Regards

    my email: ahmad.shojaeddin@gmail.com

    ReplyDelete