Friday, December 27, 2013

Triangles drawn by VBO's

It took me a while to figure out Vertex Buffer Objects, they get pretty involved and throw you into some complicated stuff.  I'm not sure I fully understand all the stuff I had to do to make them work, but the program does what I want it to do and that has to count as some kind of success.

There's a lot of stuff to cover on this one, and with the hopes of keeping the posts at reasonable sizes I'm going to break them up into a few posts.  In this one I'm going to cover how to use VBO's to get your polygons on the screen.

Last post I created a few triangles using immediate mode and vertex arrays.  None of that will be used here, so we'll start by reverting back to the OpenTK Hello World program again, you can see the code here.

We'll need a new variable in our class, an array of uints which I named cauiVBOIDs.  We'll only use one buffer in this example, but under normal circumstances I think you'll be using many more and this sets space to do so.

When we set up OpenGL in GLViewLoading() we'll want to create our buffers:
1:  public class GLForm : Form {  
2:      protected static GLControl cglGLView;  
3:      protected static Label clblLog;  
4:      protected static bool cbGLReady;  
5:      protected static uint[] cauiVBOIDs;  
6:    
7:      public static void Main()   
8:    
9:      public GLForm()  
10:    
11:      public int WriteLog(string strText)  
12:    
13:      private void GLViewLoading(object oSender, EventArgs eaArgs) {  
14:          //The GLControl is now loading  
15:          Matrix4 pmatMatrix;  
16:          float fAspect;  
17:          Vector3[] av3Triangle;  
18:    
19:          WriteLog("GLControl: Loading...");  
20:          WriteLog("OpenGL Version: " + GL.GetString(StringName.Version));  
21:    
22:          //Setup GL  
23:          GL.ClearColor(0.0f, 0.0f, 0.2f, 0.0f);  
24:          GL.Enable(EnableCap.DepthTest);  
25:    
26:          //Setup the viewport  
27:          GL.Viewport(0, 0, cglGLView.Width, cglGLView.Height);  
28:          fAspect = cglGLView.Width / cglGLView.Height;  
29:          pmatMatrix = Matrix4.CreatePerspectiveFieldOfView(MathHelper.PiOver4, fAspect, 0.1f, 100.0f);  
30:          GL.MatrixMode(MatrixMode.Projection);  
31:          GL.LoadMatrix(ref pmatMatrix);  
32:          GL.MatrixMode(MatrixMode.Modelview);  
33:    
34:          //Create video memory buffers  
35:          cauiVBOIDs = new uint[1];  
36:          GL.GenBuffers(1, cauiVBOIDs);  
37:    
38:          //Setup our vertexes  
39:          av3Triangle = new Vector3[6];  
40:    
41:          av3Triangle[0].X = 2.5f;  
42:          av3Triangle[0].Y = -1.0f;  
43:          av3Triangle[0].Z = -8.0f;  
44:          av3Triangle[1].X = 1.5f;  
45:          av3Triangle[1].Y = 1.0f;  
46:          av3Triangle[1].Z = -8.0f;  
47:          av3Triangle[2].X = 0.5f;  
48:          av3Triangle[2].Y = -1.0f;  
49:          av3Triangle[2].Z = -8.0f;  
50:    
51:          av3Triangle[3].X = -2.5f;  
52:          av3Triangle[3].Y = -1.0f;  
53:          av3Triangle[3].Z = -8.0f;  
54:          av3Triangle[4].X = -1.5f;  
55:          av3Triangle[4].Y = 1.0f;  
56:          av3Triangle[4].Z = -8.0f;  
57:          av3Triangle[5].X = -0.5f;  
58:          av3Triangle[5].Y = -1.0f;  
59:          av3Triangle[5].Z = -8.0f;  
60:    
61:          //Copy the vertex data into video memory  
62:          GL.BindBuffer(BufferTarget.ArrayBuffer, cauiVBOIDs[0]);  
63:          GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(av3Triangle.Length * Vector3.SizeInBytes), av3Triangle, BufferUsageHint.StaticDraw );  
64:    
65:          //Done setting up GL for use  
66:          cbGLReady = true;  
67:    
68:          return;  
69:      }  
70:    
71:      private void GLViewPainting(object oSender, EventArgs eaArgs)  
72:  }  


In line 5 I've established the array of uints as a class variable.  We'll need access to those in another function so this is a convenient place to keep them.  Come to think of it, once it's created we never touch the GLControl ever again it's probably safe to remove it from the class scope.

Skip down to line 35 and the array is dimensioned with only one element.  For now it's probably safe to use a scalar value, but as I mentioned in any real application you'll probably want a bunch of these buffers handy.
Line 36 is where we have OpenGL allocate the space for our buffers in video memory.  The first parameter is the number of buffers you'd like, and the second is the array which will hold the ID's OpenGL uses to reference them.  Simple enough so far.

Since these buffers are kept in video memory our program doesn't have direct access to them.  Which means we'll have to pass all of our data through OpenGL into that memory.  This is where VBO's give us a performance increase.  With the other drawing methods you had to pass your data from application memory into video memory each time you wanted to render an object.  With VBO's keeping all the information in the video card memory the only time we ever need to pass data to the video card is when something changes.  If you think about the average video game the characters constantly move, but the scenery is pretty much static.  So all the vertexes and other information for the scenery only needs to be given to the video card once.

Let's do that then.  On line 17 I added the variable av3Triangle to hold the vertexes.  On line 39 I give it 6 elements so that we can store two triangles in it.  Lines 41 through 59 just supply all the coordinates for our triangles.

On line 62 we're telling OpenGL that we want to work on one of our buffers, or rather to bind the buffer to the ArrayBuffer target.  There's a bunch of different targets we can choose from, and I'm not entirely sure when you want to use each of them.  I do know that we want an ArrayBuffer for working with a list of vertexes.

Line 63 passes our array of vertexes through OpenGL and into video memory using the GL.BufferData() function.  The first parameter specifies which target we want to work with, then we list the size of the data we're about to pass through, then the array containing the data, and finally a hint on how this data will be used.  That hint is to allow OpenGL to try and optimize the data, or at least choose the best place to store it based on how we intend to use it.

There we go, or data has been loaded into video memory and ready for us to use.  If we need to change the contents of that buffer we can call GL.BindBuffer() and GL.BufferData() again to put the new information in.

At some point we're going to want to do some drawing with that data, using it to define polygons on the screen and so forth.  This we'll do in the GLViewPainting() function like with the other methods.
1:  private void GLViewPainting(object oSender, EventArgs eaArgs) {  
2:      if (cbGLReady == false) {//Control is not loaded, do nothing  
3:          return;  
4:      }  
5:      WriteLog("Painting...");  
6:    
7:      //Clear the GL View  
8:      GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);  
9:      GL.MatrixMode(MatrixMode.Modelview);  
10:    
11:      GL.BindBuffer(BufferTarget.ArrayBuffer, cauiVBOIDs[0]);  
12:        
13:      GL.EnableVertexAttribArray(0);  
14:      GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, Vector3.SizeInBytes, 0);  
15:      GL.DrawArrays(PrimitiveType.Triangles, 0, 6);  
16:      GL.DisableVertexAttribArray(0);  
17:    
18:      //Drawing complete  
19:      cglGLView.SwapBuffers();  
20:    
21:      return;  
22:  }  

As you can see there isn't a lot of code required here.  Once again we start by binding the buffer we want to work with using GL.BindBuffer().

Now we need to explain to OpenGL how to read the data we just gave it, or at least how it should traverse the information.  For that we'll need to create a vertex attribute for our buffer.  This is done on line 13.  The parameter passed to GL.EnableVertexAttribArray() is the identifier for this set of vertex attributes.

The call to GL.VertexAttribPointer() on line 14 is where we specify all the details of the array OpenGL needs to know.  The first parameter is the identifier for the set of attributes, then the number of values for each vertex, then the type of data stored in those values, then we say if we want the data normalizes (true) or kept as fixed values (false), the size of each element, and lastly you can supply a pointer to the first element in the array.  I'm not sure when you need to specify that first element, the default is zero so I guess you only need that is unusual situations.

Line 15 does the rendering with a call to GL.DrawArrays().  We just tell it the shape of the object to draw, what element in the buffer to start with, then the number of vertexes to put on screen.

There is a limited number of vertex attributes you can have, so when you're done with them you should clean up using GL.DisableVertexAttribArray() and tell it the ID to remove.

Once all this is in place your program should create a form that looks like this:

Well, it'll look like that if you have an NVidia video card.  If you happen to be using something else then you probably won't see any triangles at all.  NVidia appears to be more friendly with shaders than others.

If you do see the triangles they are somewhat less interesting than before since there's no color.  In order to get colors on polygons using VBO's we need to create some shaders.  For those that don't see triangles at all these same shaders will have them appear on your screen.

Shaders need to be written in GLSL, loaded by your program, and compiled at run time.  This took me quite a while to figure out, and I'm just hoping I can explain it without having to make too much up.

For the complete code in this example go here.

No comments:

Post a Comment