Sunday, February 5, 2017

OpenGL 4 with OpenTK in C# Part 8: Drawing multiple objects

In this post we will look at how to reuse the Vertex Array and Buffers to draw the same model multiple times.

This is part 8 of my series on OpenGL4 with OpenTK.
For other posts in this series:
OpenGL 4 with OpenTK in C# Part 1: Initialize the GameWindow
OpenGL 4 with OpenTK in C# Part 2: Compiling shaders and linking them
OpenGL 4 with OpenTK in C# Part 3: Passing data to shaders
OpenGL 4 with OpenTK in C# Part 4: Refactoring and adding error handling
OpenGL 4 with OpenTK in C# Part 5: Buffers and Triangle
OpenGL 4 with OpenTK in C# Part 6: Rotations and Movement of objects
OpenGL 4 with OpenTK in C# Part 7: Vectors and Matrices
OpenGL 4 with OpenTK in C# Part 8: Drawing multiple objects
OpenGL 4 with OpenTK in C# Part 9: Texturing
OpenGL 4 with OpenTK in C# Part 10: Asteroid Invaders
Basic bullet movement patterns in Asteroid Invaders
OpenGL 4 with OpenTK in C# Part 11: Mipmap
OpenGL 4 with OpenTK in C# Part 12: Basic Moveable Camera
OpenGL 4 with OpenTK in C# Part 13: IcoSphere
OpenGL 4 with OpenTK in C# Part 14: Basic Text
OpenGL 4 with OpenTK in C# Part 15: Object picking by mouse

As stated in the previous post, I am in no way an expert in OpenGL. I write these posts as a way to learn and if someone else finds these posts useful then all the better :)
If you think that the progress is slow, then know that I am a slow learner :P
This part will build upon the game window and shaders from part 6..

Projection Matrix

As a starter, in the previous post the cube looked a little odd. It had no real perspective and when the window was rescaled it didn't behave as it should have. I.e. it got out of shape.
To solve this, we shall add a Projection Matrix to our solution.
In our vertex shader, we will add another uniform matrix in addition to the modelView matrix we already have as follows:
#version 450 core

layout (location = 0) in vec4 position;
layout(location = 1) in vec4 color;

out vec4 vs_color;

layout(location = 20) uniform  mat4 projection;
layout (location = 21) uniform  mat4 modelView;

void main(void)
{
 gl_Position = projection * modelView * position;
 vs_color = color;
}

Be sure to get the order of multiplications correct or else things will look very very strange.
In our GameWindow, lets add the following method to create this projection matrix.
private void CreateProjection()
{
            
    var aspectRatio = (float)Width/Height;
    _projectionMatrix = Matrix4.CreatePerspectiveFieldOfView(
        60*((float) Math.PI/180f), // field of view angle, in radians
        aspectRatio,                // current window aspect ratio
        0.1f,                       // near plane
        4000f);                     // far plane
}
Basically we tell OpenTKs Matrix4 to create this matrix for us. And we want to supply the angle of the field of view together with the window aspect ratio and the near and far clipping planes.
Changing the angle is a cheap mans zoom of sorts, the aspect ratio helps keep things in shape even if we drag the window to strange sizes and the near and far clipping determine how close or far away an object can be and still be drawn on screen.
We want to call the CreateProjection from both the OnLoad and OnResize event handlers.
Then in our OnRenderFrame add a call to
GL.UniformMatrix4(20, false, ref _projectionMatrix);
And we should be set.

Drawing multiple objects

So, now that we have gone through the projection matrix that should probably have been covered in an earlier post, but I did not know it at the time of writing we can finally start with the topic of this post: Drawing multiple objects on screen.

Lets initialize a few different RenderObjects in out OnLoad method. As the only model we have at the moment is a cube, lets create some different colored ones so that we know the difference.
In reality these would probably be different models that look differently all together. But for now lets take what we have. But you can imagine that HotPink is a car, BlueViolet is a truck, Red is a Motorcycle and LimeGreen is a lawnmower ;)
protected override void OnLoad(EventArgs e)
{
    VSync = VSyncMode.Off;
    CreateProjection();
    _renderObjects.Add(new RenderObject(ObjectFactory.CreateSolidCube(0.2f, Color4.HotPink)));
    _renderObjects.Add(new RenderObject(ObjectFactory.CreateSolidCube(0.2f, Color4.BlueViolet)));
    _renderObjects.Add(new RenderObject(ObjectFactory.CreateSolidCube(0.2f, Color4.Red)));
    _renderObjects.Add(new RenderObject(ObjectFactory.CreateSolidCube(0.2f, Color4.LimeGreen)));

    CursorVisible = true;

    _program = CreateProgram();
    GL.PolygonMode(MaterialFace.FrontAndBack, PolygonMode.Line);
    GL.PatchParameter(PatchParameterInt.PatchVertices, 3);
    GL.Enable(EnableCap.DepthTest);
    Closed += OnClosed;
}

So, now we have 4 different RenderObjects created, each containing Vertex Arrays and Buffers. Next step is to draw them on the screen. But we don't want to just draw one of each, lets draw a bunch.
protected override void OnRenderFrame(FrameEventArgs e)
{
    _time += e.Time;
    Title = $"{_title}: (Vsync: {VSync}) FPS: {1f / e.Time:0}";
    GL.ClearColor(_backColor);
    GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

    GL.UseProgram(_program);
    GL.UniformMatrix4(20, false, ref _projectionMatrix);
    float c = 0f;
    foreach (var renderObject in _renderObjects)
    {
        renderObject.Bind();
        for (int i = 0; i < 5; i++)
        {
            var k = i + (float)(_time * (0.05f + (0.1 * c)));
            var t2 = Matrix4.CreateTranslation(
                (float)(Math.Sin(k * 5f) * (c + 0.5f)),
                (float)(Math.Cos(k * 5f) * (c + 0.5f)),
                -2.7f);
            var r1 = Matrix4.CreateRotationX(k * 13.0f + i);
            var r2 = Matrix4.CreateRotationY(k * 13.0f + i);
            var r3 = Matrix4.CreateRotationZ(k * 3.0f + i);
            var modelView = r1 * r2 * r3 * t2;
            GL.UniformMatrix4(21, false, ref modelView);
            renderObject.Render();
        }
        c += 0.3f;
    }
    GL.PointSize(10);
    SwapBuffers();
}
The projection matrix will stay the same during the whole frame, so we need only to set it once before the render loop.
Then for each model that we loaded (different colored cube in our case), lets apply some transforms and rotations, each object gets a unique model view matrix that we set.
Note that the same RenderObject is bound once but rendered multiple times, just with different translations.
In the RenderObject, we need to split the old Render method into a Render and Bind method for this to work.
public void Bind()
{
    GL.BindVertexArray(_vertexArray);
}
public void Render()
{
    GL.DrawArrays(PrimitiveType.Triangles, 0, _verticeCount);
}

The end results should be like this:


For the full source at the end of part 8, go to: https://github.com/eowind/dreamstatecoding

Hope this helps someone out there :)
No cat video today either, it seems to be a every other post thing currently.


11 comments:

  1. Interestingly this only works for me when i set t2 `z` to a negative value.

    ReplyDelete
    Replies
    1. Hi there, thank you for pointing this out! On the code uploaded to github it is correctly -2.7. But thanks for pointing it out, I will update this post as it seems like other people have issues with this as well.

      Delete
  2. Same here, almost lost my mind trying to find out why I only see a dark blue background. Thank you so much :)

    ReplyDelete
    Replies
    1. Hi there, sorry for the late answer, Somehow I have missed all the comments on this post. The code in the post has been updated, the code on github has had negative z value from the start. Sorry for the inconvenience.

      Delete
  3. At the end of this part, all I got was a dark blue background, and I cant figure out why, I copied the source code from github directly and it still wont work, yet there are no errors. WHATS UP!?

    ReplyDelete
    Replies
    1. Hi there, thanks for commenting!
      I am unsure what could be wrong if you don't get it working with the code on github. Have you tried looking at the z value of the t2 variable in the OnRenderFrame method.. Just to make sure that the objects are on the correct side of the camera!

      Delete
  4. How do you enable color if you use vertexshader and fragment with current color?

    I have downloaded github. I really don't understand why does color not work for me? I have tried

    CHeck my glist: https://gist.github.com/sourceskyboxer/918c20b9cd4115949e998087ac981c11

    Can you help me how do I fix with triangle if triangle should be colorable. Like your Vertex from your github but I replace to Vector3. But it can't work because I have already tried with shaders ( in ChangeTriangleAndBackgroundWindow -> vertexSource ( private string ) It looks like same to your vertexshader but I use only Vector3 and Color4.

    Thanks! I hope you have to resolve my changing triangle's color.

    ReplyDelete
    Replies
    1. Yeah it is resolved because you didn't tell me "layout (location 1) in vec4 color" It is wrong because "in" should replace to "uniform" because color can able changing color.

      Delete
    2. https://www.youtube.com/watch?v=3G0Au5s4YFc

      Delete
    3. Hi there. I'm glad that it worked out for you.

      Delete
  5. Excellent article and this helps to enhance your knowledge regarding new things. Waiting for more updates.
    Angular 11 New Features
    Angular Latest Stable Version

    ReplyDelete