Friday, November 8, 2013

OpenTK Tutorial 4: It's amazing the way you DRAW TWO THINGS

In this tutorial, we'll build on our cube by making a second one. Along the way, we'll be remaking some of the old code to be more object-oriented to fit our future needs.

(I'd like to thank an anonymous commenter on the last tutorial for inspiring this one, since originally I was going to do texturing or a camera first)



We'll be starting from the end of Tutorial 3, where we had one colorful cube, spinning. The first thing we'll be doing is making it object oriented. This will make adding more objects and different types of objects later much easier.

Make a new class, and call it Volume. This isn't volume as in loudness, but a volume as in space being taken up. Technically "volume rendering" is something else, but I couldn't think of a better name. We may end up changing it in some refactoring later, so don't get too attached to the name.

Volume is going to be different than our other classes. It's going to be an abstract class, since we can't say much for a generic concept of a shape. It wouldn't have any vertices, but we want to use it for polymorphism. This means we can have a list of Volumes (which are actually subtypes, like Cubes will be) and when we call GetVerts, it'll return the correct vertices, despite us not checking the type in our code (very useful).

Here we have our Volume class (make sure you add a using directive for OpenTK):

public abstract class Volume

    {

        public Vector3 Position = Vector3.Zero;
        public Vector3 Rotation = Vector3.Zero;
        public Vector3 Scale = Vector3.One;

        public int VertCount;
        public int IndiceCount;
        public int ColorDataCount;
        public Matrix4 ModelMatrix = Matrix4.Identity;
        public Matrix4 ViewProjectionMatrix = Matrix4.Identity;
        public Matrix4 ModelViewProjectionMatrix = Matrix4.Identity;

        public abstract Vector3[] GetVerts();
        public abstract int[] GetIndices(int offset = 0);
        public abstract Vector3[] GetColorData();
        public abstract void CalculateModelMatrix();
    }

What we're doing here is storing transformation information, in addition to providing methods to get our lists of vertices, indices, colors (since we aren't using textures yet), the lengths of these lists, a place to store our matrices, and a method to use our transformations to make a transformation matrix for the whole object.

By itself, this has no implemented code. We only have definitions for methods, so we'll need to define them in another class.

Make another new class, and call it Cube. In Cube, we'll be filling in these functions for a cube (and since our scale transformation is a Vector3, it can actually be any rectangular prism we want it to be).

First, set Cube to be a subclass of Volume:

public class Cube : Volume

Now, we can set values for VertCount, IndiceCount and ColorDataCount in its constructor:

        public Cube()
        {
            VertCount = 8;
            IndiceCount = 36;
            ColorDataCount = 8;
        }

The other methods are mostly moving code we had in the Game class, and moving them to other classes so we can use some object-oriented tricks to make the code more versatile.

GetVerts returns the same vertices we used for the cube before, but for a cube that's 1 unit in each dimension instead of 1.6:

        public override Vector3[] GetVerts()
        {
            return new Vector3[] {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),
                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),
            };
        }

Seems simple enough. Now we need to do the same for indices:

        public override int[] GetIndices(int offset = 0)
        {
            int[] inds = new int[] {
                //left
                0, 2, 1,
                0, 3, 2,
                //back
                1, 2, 6,
                6, 5, 1,
                //right
                4, 5, 6,
                6, 7, 4,
                //top
                2, 3, 6,
                6, 3, 7,
                //front
                0, 7, 3,
                0, 4, 7,
                //bottom
                0, 1, 5,
                0, 5, 4
            };



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

            return inds;
        }

There's a big change to this code. As you may have noticed in the abstract class, this method takes a parameter, for "offset". Right now it's not that important, since every object being drawn is the same cube (the vertices are the same, even though we later apply a transformation), but when we have multiple types of objects, this reuse would become a disaster.

Color data is pretty straightforward:

        public override Vector3[] GetColorData()
        {
            return 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)
            };
        }

It simply returns the colors we used for the cube before.

The model matrix is more complex, but it's just a more generic version of code from the last tutorial:

public override void CalculateModelMatrix()

        {
            ModelMatrix = Matrix4.Scale(Scale) * Matrix4.CreateRotationX(Rotation.X) * Matrix4.CreateRotationY(Rotation.Y) * Matrix4.CreateRotationZ(Rotation.Z) * Matrix4.CreateTranslation(Position);
        }
Now the Cube class is complete. We just need to change our Game class to use it instead and let us render more than one.

We won't be needing the mviewdata variable anymore, so delete that, and in its place add:

        List<Volume> objects = new List<Volume>();

This List will store everything we're going to draw, including what was previously in mviewdata. I used a List instead of an array to make the program more dynamic. If you're looking at Visual Studio's Error List right now, you'll probably see a lot of issues that just popped up. We'll be fixing those next.

In initProgram, we'll want to give it something to draw:

            objects.Add(new Cube());
            objects.Add(new Cube());

Right now, the OnLoad method is a mess of pre-defined values. We don't need those anymore, so it should look like this (feel free to change the title if you want):

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            initProgram();

            Title = "Hello OpenTK!";
            GL.ClearColor(Color.CornflowerBlue);
            GL.PointSize(5f);
        }

The next change is to OnRenderFrame. Right now it renders everything in the arrays given to the graphics card at once. We need to change our model-view-projection matrix for each object, so it can't just draw them all at once. To do this, we can use the GL.DrawRangeElements method, which can render a subset of the indices we have.

We'll also want to do this in a foreach loop, so we can use our list to the fullest. Replace the call to GL.DrawElements with the following:

            int indiceat = 0;
           
            foreach (Volume v in objects)
            {
                GL.UniformMatrix4(uniform_mview, false, ref v.ModelViewProjectionMatrix);
                GL.DrawElements(BeginMode.Triangles, v.IndiceCount, DrawElementsType.UnsignedInt, indiceat * sizeof(uint));
                indiceat += v.IndiceCount;
            }

This code iterates through the objects and draws each of them individually.

We have one last bit of code to fix the last of the errors our first change brought up. After the call to base.OnUpdateFrame, add this:

            List<Vector3> verts = new List<Vector3>();
            List<int> inds = new List<int>();
            List<Vector3> colors = new List<Vector3>();

           
            int vertcount = 0;
            
            foreach (Volume v in objects)
            {
                verts.AddRange(v.GetVerts().ToList());
                inds.AddRange(v.GetIndices(vertcount).ToList());
                colors.AddRange(v.GetColorData().ToList());
                vertcount += v.VertCount;
            }

            vertdata = verts.ToArray();
            indicedata = inds.ToArray();
            coldata = colors.ToArray();

In this code, we gather up all the values for the data we need to send to the graphics card.

After we add to the time variable, we have two lines which still refer to mviewdata (one updates it to new values, the other sends it to the graphics card with GL.UniformMatrix4). Replace them with the following:


            objects[0].Position = new Vector3(0.3f, -0.5f + (float) Math.Sin(time), -3.0f);
            objects[0].Rotation = new Vector3(0.55f * time, 0.25f * time, 0);
            objects[0].Scale = new Vector3(0.1f, 0.1f, 0.1f);

            objects[1].Position = new Vector3(-1f, 0.5f + (float)Math.Cos(time), -2.0f);
            objects[1].Rotation = new Vector3(-0.25f * time, -0.35f * time, 0);
            objects[1].Scale = new Vector3(0.25f, 0.25f, 0.25f);

            foreach (Volume v in objects)
            {
                v.CalculateModelMatrix();
                v.ViewProjectionMatrix = Matrix4.CreatePerspectiveFieldOfView(1.3f, ClientSize.Width / (float)ClientSize.Height, 1.0f, 40.0f);
                v.ModelViewProjectionMatrix = v.ModelMatrix * v.ViewProjectionMatrix;
            }
This code makes the cubes move around and spin, and also updates their model-view-projection matrices. With this code, you'll see the following output:


41 comments:

  1. Just starting with OpenGL/OpenTK programming. Thank you for this awesome tutorial!

    ReplyDelete
  2. Please keep going, these tutorials are awesome!

    ReplyDelete
  3. Hi! Great Tutorial! But I have been wondering and trying to figure out; How can I create two individual objects?

    I tried making another class similar to Cube but with a different color.
    But when i try to draw them, Instead of seeing two different cubes
    I only see Similar Cubes.

    This is how I add them:

    objects.Add(new Cube());
    objects.Add(new Cube2());

    and how I draw them:
    objects[0].Position = new Vector3(0.0f, -0.8f, -3.0f);
    objects[0].Rotation = new Vector3(0, 0.25f * time, 0);
    objects[0].Scale = new Vector3(1f, 1f, 1f);

    objects[1].Position = new Vector3(0.0f, 0.2f, -3.0f);
    objects[1].Rotation = new Vector3(0, 0.25f * time, 0);
    objects[1].Scale = new Vector3(1f, 1f, 1f);

    i cant figure it out

    ReplyDelete
    Replies
    1. OH is it because of the "offset" parameter? "as you mention in the tutorial, it'll be a disaster when it comes to multiple different objects"

      If so, how can I change this? Im still trying to figure out

      Delete
    2. There was a small issue with the code. Thanks for helping find it. The updated post will be up shortly (look for the change in the OnRenderFrame function).

      Delete
    3. (There's also a change to OnUpdateFrame, by the way)

      Delete
    4. Oh Thank you!!! I've been waiting for the update! Thank you so much!!! Keep up the good work

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

    ReplyDelete
  5. Thank you for a great tutorial, but is there a place where I can download the code to read through it at my own leisure?

    ReplyDelete
  6. Awesome work! Thanks man!

    ReplyDelete
  7. salam, peace

    brother I want to make video tutorials to learn Modern OpenGL using OpenTK Please..
    and I hope make to learn us everything

    Thanks, salam.

    ReplyDelete
  8. Quick question: I'm working on a 2d ortho project. I've modified the vs.glsl shader program from "in vec3 vPosition" to "in vec2 vPosition" and the main function to

    vec3 finalPos = vec3(vPosition,0.1);
    gl_Position = modelview * vec4(finalPos, 1.0);

    I also adapted the rest of the code (objects, the VBO VertexAttribPointer etc.) to accept 2D vertex input instead of 3D for easier coding and (perhaps) performance. Was this stupid? Should I keep the z-level for some reason. I predict that this might be a problem with layering later on, no?

    Thanks!

    Pete

    ReplyDelete
    Replies
    1. Hi Pete,

      I'd recommend keeping the z-level for simpler layering, but it's not mandatory at all. If you use GL.DepthFunc to change the depth testing function, you can control ordering to an extent without changing z values. Not sending a z value should improve performance (or at least memory use), so you may want to see if a different depth test can work for you.

      Delete
    2. Great, thanks! I'll play around with it a bit.

      Pete

      Delete
  9. There seems to be a mistake in the code sample for DrawRangeElements() (as at 20JUN2014). It still shows the original DrawElements() call. It should be:

    GL.DrawRangeElements(PrimitiveType.Triangles, v.IndiceCount, indiceat, indiceat + v.IndiceCount - 1, v.IndiceCount, DrawElementsType.UnsignedInt, indiceat * sizeof(uint));

    ReplyDelete
    Replies
    1. This wasnt working for me cause i had no overload for 7 params (probably old lib) So ive used

      GL.DrawRangeElements(BeginMode.Triangles, indiceat, indiceat + v.IndiceCount - 1, v.IndiceCount, DrawElementsType.UnsignedInt, v.GetIndices());

      Delete
  10. Can you give a download link for this project, I am completely lost at the end part.

    ReplyDelete
  11. Please post the code somewhere for this thing at the end of the tutorial, or in a separate file. Thanks to this, I accidentally broke my code and I can't get it back thanks to all of the 'Replace' stuff. Please post the code. Other than that, the code is understandable but some of it is a bit obsolete.

    ReplyDelete
  12. Hi. Nice and great. Just you need to update : Matrix4.Scale to ---> Matrix.CreateScale(...) so you won't get green mark flag. Thanks Abdol with VS2010.

    ReplyDelete
  13. Is it possible to store the vertex information for each object in separate arrays, then pass these to the shader individually, opposed to how you combine the vertex info for each object into one large buffer?

    If this is possible, what GL methods should I look for? I've tried looking at the GL documentation, but I'm not entirely sure what I'm looking for.

    This would make more sense to me from a data management standpoint, and it seems like keeping the buffers separate would make it easier to use multiple shaders. Why do you combine vertex data into larger arrays rather than other approaches?

    Thanks again for making these tutorials and being so helpful to people who ask questions.

    ReplyDelete
  14. This comment has been removed by the author.

    ReplyDelete
  15. Hi there, I'm getting an AccessViolationException at the DrawElement line:

    GL.DrawElements(BeginMode.Triangles, v.IndiceCount, DrawElementsType.UnsignedInt, indiceat * sizeof(uint));

    I'm not sure what I did wrong, since I tried to follow your tutorial exactly. Could you give me some pointers please?

    (repost since I forgot to check the "notify me" option last time)

    ReplyDelete
    Replies
    1. Hi tta269,

      Can you post your entire Game.cs file for me? It would help debug it.

      Delete
    2. Hi Kabuto,

      Thanks for writing back to me. Turned out it was because I set different frequency for the updates-per-second and frames-per-second.

      game.Run(30, 60);

      I set both to 30 and everything is fine now.

      Delete
    3. I have also experienced the same problem (due to the different refresh rate between OnUpdateFrame() and OnRenderFrame()). I find two solutions to this. (1) change "game.Run(30.0);" into "game.Run(30.0, 30.0);" (2) make sure you have runned OnUpdateFrame() once before running OnRenderFrame().

      And your tutorial is really helpful, thank you so much.

      Delete
  16. Hi!
    Being an absolute beginner, I love your tutorials.

    I want to draw a few cylinders; but I'm having trouble displaying even one cylinder.
    I'm trying to follow your approach. Check out my project:

    https://goo.gl/v9kxvJ

    (Kindly ignore the "Form.." files)

    Thanks in advance!

    ReplyDelete
    Replies
    1. Hi Govinda,

      It looks like your project file isn't set to be shared publicly. I've sent a request for access from my email (neokabuto@gmail.com) and I'll be glad to help as soon as I can see the files.

      Delete
    2. Hi Govinda,

      You need to add a call to initProgram in your OnLoad function.

      You can also optimize your custom shape by having the code to create the color, vertex, and index lists in the constructor (how they are right now, it duplicates the model on top of itself every frame, using more and more memory).

      Delete
    3. One other thing, you'll need to set the zNear on the call to CreatePerspectiveFieldOfView in OnUpdateFrame to something like 1. A negative number throws an exception.

      Delete
    4. Thank you so much for your feedback. I really appreciate that. :)

      For the time being, I did the task in AutoCAD, but I will definitely try it using OpenTK also.

      Cheers!

      Delete
  17. All of your tutorials so far have been amazing. Thank you so much for putting these together!!

    ReplyDelete
  18. Hi, I just read another webpage introducing how the three matrices actually work in OpenGL when view in 3d space (http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/), which seems just opposite from yours. Two important things it mentioned are "we need to perform the scaling FIRST, and THEN the rotation, and THEN the translation" and this image "http://www.opengl-tutorial.org/assets/images/tuto-3-matrix/MVP.png". You know the matrix multiplication is actually in the opposite order compare to the transformation we want. So what do you think about this thing? (I think maybe you put every matrix in a transposed format at the very beginning, so every thing looks just fine)

    ReplyDelete
    Replies
    1. The object is not visible if I set the modelview matrix with code below... I don't know why...
      {
      Matrix4 m = Matrix4.Identity;
      Matrix4 cameraDirecT = Matrix4.LookAt(new Vector3(0, 0, 3), Vector3.Zero, Vector3.UnitY);
      m = cameraDirecT * m;
      Matrix4 p = Matrix4.CreatePerspectiveOffCenter(-1.0f / 2, 1.0f / 2, -(Height / 2.0f) / Width, (Height / 2.0f) / Width, 1, Width);
      Matrix4 MVPMatrix = p * m;
      GL.UniformMatrix4(shaders[activeShader].GetUniform("modelView"), false, ref MVPMatrix);
      }

      Delete
    2. I find someone else also had this problem, and the answer is "There is a difference between column-major and row-major storage of matrices in the memory". (reference: http://www.opentk.com/node/2794)

      Delete
    3. Hi Regis,

      That's the difference. OpenTK has its matrices transposed, so matrix code that would work with other OpenGL libraries needs to be changed to work in it.

      Delete
  19. Hello,

    The previous rotating cube tutorial works, however the cubes do not appear on this tutorial, i just get a blank cornflowerblue screen, please help :(

    thank you!

    ReplyDelete
    Replies
    1. Hi Jai,

      Can you post your code (on something like PasteBin)?

      Delete
  20. Hello,

    The previous rotating cube tutorial works, however the cubes do not appear on this tutorial, i just get a blank cornflowerblue screen, please help :(

    thank you!

    ReplyDelete
  21. I am having problems with my code and i dont know what is wrong this is my code
    http://pastebin.com/cPUGwRiu

    ReplyDelete
  22. You should change "Matrix4.Scale(...)" to "Matrix4.CreateScale(...)" as it is acsolute

    ReplyDelete
    Replies
    1. I'm going to work on updating the tutorials for OpenTK 2.0 as soon as I can, but for now if you want to see a list of similar changes, check out this GitHub commit: https://github.com/neokabuto/OpenTKTutorialContent/commit/d5239419daa6fda45d868b1c137c464bd3e55633

      Delete