Monday, December 23, 2013

Shapes and colors in GL

Previously I went through how to create a Windows form with an GLControl in it.  Nothing was drawn in that control, and really whats the point of having OpenGL accessible in your program if you don't actually draw anything?

There are several ways you can tell OpenGL to draw polygons.
  • Immediate mode is the oldest and probably the slowest method of drawing.  They are not included in OpenGL ES, which is what Android and iOS devices use.
  • Vertex Arrays was introduced in version 1.1.  This allows you to pass chunks of data to OpenGL in a single function.  All the data is stored in program memory which means that it needs passed through to video memory in order to render anything, this hampers performance quite a bit.
  • Vertex Buffer Objects were released with version 1.5.  This moved the information needed for rendering into video memory which boosted performance.  I believe this is the preferred method of rendering objects currently.
When I was first working with OpenGL my machine had an integrated Intel video card in it.  The drivers only provided OpenGL version 1.4 which blocked all of my efforts to figure out vertex buffer objects.  Fortunately, that video card has been replaced with something better and I can play with modern methods.  I'm still going to take some time and go over the old methods just for completeness.
If we start with the code from last time then almost all of the changes needed are in the GLViewPainting() function.  This was the one that was called when the GLControl needed redrawn.  All the rest of the code can remain the same.

Let's take a look at immediate mode first.  Here's how you would draw a triangle:
1:  GL.Begin(BeginMode.Triangles);  
2:  GL.Vertex3(-0.5f, -1.0f, -8.0f);  
3:  GL.Vertex3(-1.5f, 1.0f, -8.0f);  
4:  GL.Vertex3(-2.5f, -1.0f, -8.0f);  
5:  GL.End();  

We start by saying we'd like to begin drawing, and specifically that we'd like to draw triangles, with a call to GL.Begin().  You can also work with polygons that have 4 or more sides but there are rules about how they can be formed; you can have the lines intersect, the shape must be entirely convex, etc.  Triangles can't be formed wrong so I just stick with those.

The GL.Vertex3() function passes a single vertex to OpenGL, we tell it in order the X, Y, and Z coordinates.  The X axis is left to right, Y is up and down, and Z is far and near.  Positive X is to the right, positive Y is up, and positive Z is behind you.  So to get our triangle in front of the camera we need a negative Z value.

When we're done setting all of our vertexes we call GL.End().  You can pass a bunch of triangles in between calls to GL.Begin() and GL.End(); and you can also call GL.Begin() and GL.End() multiple times.  There's no limits imposed on them.

Since we haven't specified any colors OpenGL defaults to drawing in white, so you'll see a white triangle on the left side of the GLControl.  If we want to specify colors for our triangles we have to call GL.Color4() function to set our drawing color.  Then all vertexes will be drawn in that color, if you want different colors then you just call GL.Color4() each time you want to change colors.
1:  GL.Begin(BeginMode.Triangles);  
2:  GL.Color4(1.0f, 0.0f, 0.0f, 1.0f);  
3:  GL.Vertex3(-0.5f, -1.0f, -8.0f);  
4:  GL.Color4(0.0f, 1.0f, 0.0f, 1.0f);  
5:  GL.Vertex3(-1.5f, 1.0f, -8.0f);  
6:  GL.Color4(0.0f, 0.0f, 1.0f, 1.0f);  
7:  GL.Vertex3(-2.5f, -1.0f, -8.0f);  
8:  GL.End();  

OpenGL uses four values to set the color.  Red, green, blue, and transparency in that order.  I guess transparency is actually the alpha value, I should get in the habit of using the correct terms.  There is a GL.Color3() function which only needs the red, green, and blue values.  I figured it might be useful to be in the habit of using all four so it wouldn't be strange when I needed to have something be partially transparent; I'm not sure if that is actually useful or not though.

As you might guess it would be pretty tedious to pass all your vertexes one at a time, not to mention very slow.  Which is where vertex arrays come in.  With them you create arrays of vertexes and pass all that data in with a single function call.

We will need to enable the VertexArray feature of OpenGL to use them.  To do that we add this line to the function that sets up OpenGL for us, in my example its the GLViewLoading() function.:
1:  GL.EnableClientState(ArrayCap.VertexArray);


With that done, here's the code to use vertex arrays:
1:  Vector3[] v3Triangle;  
2:    
3:  v3Triangle = new Vector3[3];  
4:    
5:  v3Triangle[0].X = 2.5f;  
6:  v3Triangle[0].Y = -1.0f;  
7:  v3Triangle[0].Z = -8.0f;  
8:  v3Triangle[1].X = 1.5f;  
9:  v3Triangle[1].Y = 1.0f;  
10:  v3Triangle[1].Z = -8.0f;  
11:  v3Triangle[2].X = 0.5f;  
12:  v3Triangle[2].Y = -1.0f;  
13:  v3Triangle[2].Z = -8.0f;  
14:    
15:  GL.VertexPointer(3, VertexPointerType.Float, BlittableValueType.StrideOf(v3Triangle), v3Triangle);  
16:  GL.DrawArrays(BeginMode.Triangles, 0, v3Triangle.Length);  

Lines 1 through 13 create an array of Vector3 objects which will hold all of our vertexes, then fill in each of the coordinates.

In line 15 we share our vertex array with OpenGL.  The first parameter states the number of coordinates our array has.  The second parameter specifies what type of value we'll be passing, the Vector3 structure uses floats so that's what we'll tell OpenGL.  The third parameter is a little more complicated.  OpenGL is a C API, and so it expects C style arrays.  We're not using C, so we need to help OpenGL adapt to C# arrays.  What we're specifying here is the stride of the array, what that means is the number of bytes between consecutive vertices.  With that OpenGL will be able to interpret the C# array correctly.  Finally the last parameter is the array itself.

Line 16 lets OpenGL know that we're done passing through data, and in needs to render the polygons now.  We tell it the type of shape we're specifying, in this case triangles, the index of the array to start with, and the number of elements to use.

If you're going to specify colors we'll need to enable a feature for them as well.  Once add a line to the function that sets up OpenGL:
1:  GL.EnableClientState(ArrayCap.ColorArray);  

 then they'll need to be set for every vertex.
1:  Vector3[] v3Triangle;  
2:  Vector4[] v4Colors;  
3:    
4:  v3Triangle = new Vector3[3];  
5:  v4Colors = new Vector4[3];  
6:    
7:  v3Triangle[0].X = 2.5f;  
8:  v3Triangle[0].Y = -1.0f;  
9:  v3Triangle[0].Z = -8.0f;  
10:  v3Triangle[1].X = 1.5f;  
11:  v3Triangle[1].Y = 1.0f;  
12:  v3Triangle[1].Z = -8.0f;  
13:  v3Triangle[2].X = 0.5f;  
14:  v3Triangle[2].Y = -1.0f;  
15:  v3Triangle[2].Z = -8.0f;  
16:    
17:  v4Colors[0].X = 1.0f;  
18:  v4Colors[0].Y = 0.0f;  
19:  v4Colors[0].Z = -0.0f;  
20:  v4Colors[0].W = 1.0f;  
21:  v4Colors[1].X = 0.0f;  
22:  v4Colors[1].Y = 1.0f;  
23:  v4Colors[1].Z = 0.0f;  
24:  v4Colors[1].W = 1.0f;  
25:  v4Colors[2].X = 0.0f;  
26:  v4Colors[2].Y = 0.0f;  
27:  v4Colors[2].Z = 1.0f;  
28:  v4Colors[2].W = 1.0f;  
29:    
30:  GL.VertexPointer(3, VertexPointerType.Float, BlittableValueType.StrideOf(v3Triangle), v3Triangle);  
31:  GL.ColorPointer(4, ColorPointerType.Float, BlittableValueType.StrideOf(v4Colors), v4Colors);  
32:  GL.DrawArrays(BeginMode.Triangles, 0, v3Triangle.Length);  

This time we use a Vector4 object since it holds four values.  Unfortunately the names of that object don't line up very well.  X is red, Y is green, Z is blue, and W is alpha.

Line 31 submits this array to OpenGL using GL.ColorPointer().  The parameters of GL.ColorPointer() work the same as GL.VertexPointer().

It's worth mentioning that .Net is a garbage collected environment this can get a little risky.  OpenGL does some actions asynchronously, so it's hard to know exactly when it will be working with your array.  If the garbage collector comes along and moves or changes that array then you'll get random access violation exceptions.  To prevent this you can call GL.Finish() to wait until rendering is complete.  This will cause a performance hit since you're forcing your program and OpenGL to sync up.

If we tuck all of this drawing code into out rendering function, GLViewPainting(), after we clear the drawing buffer and before we swap our drawing and displayed buffers we should end up with something that looks like this:

On the plus side we can draw colored polygons onto the screen, and by changing the coordinates each time we render them they'll appear to move around.  On the negative side we're doing this with some deprecated functions and the compiler will warn us every time it looks at this code.  I still have to work out exactly how vertex buffer objects work, but once those are used to render our triangles we should be in great shape.

You can see the full source for this program here.


No comments:

Post a Comment