I should have known better since this has been done in Java, which is essentially the same sort of setup. Sure enough I was proved wrong. I got into the Hex: Shards of Fate alpha from being a Kickstarter backer and soon discovered not only was this game written in .Net but it used the Mono framework as well. The graphics appear to be going through the Unity graphics library as well. All of the .Net parts they are using can be used on mobile devices as well, presumably this is how Hex will be getting onto those platforms as well. From a technical standpoint I'm very curious how this game will end up.
From a more personal standpoint its rekindled my interest in using OpenGL within a .Net application. With that in mind let me dust off that old code and see if I can't figure out how to make it work again.
The library I've been using to connect C# with OpenGL is OpenTK. OpenTK is pretty basic, all it does is provide direct access to the OpenGL functions. There's a bunch of other choices, but most seem to be entire frameworks for doing the graphics. I don't generally like using frameworks since they tend to have a specific workflow that they push you towards. I like doing things my way and since I don't have any deadline for this stuff I'm ok with building up my own frameworks.
We'll need OpenTK.dll and the OpenTK.GLControl.dll referenced when we do the compile. I'll also be using windows forms so you'll need to reference that if you're working in Mono. In the code we'll need the OpenTK namespace to give us the GLControl class, and the OpenTK.Graphics.OpenGL namespace to get to all the OpenGL functions.
Let's setup the class first, then I'll go through each function individually.
1: using System;
2: using System.Windows.Forms;
3: using OpenTK;
4: using OpenTK.Graphics.OpenGL;
5:
6: namespace OpenTKTest {
7: public class GLForm : Form {
8: protected static GLControl cglGLView;
9: protected static Label clblLog;
10: protected static bool cbGLReady;
11:
12: public static void Main() {
13: }
14:
15: public GLForm() {
16: }
17:
18: public int WriteLog(string strText) {
19: }
20:
21: private void GLViewLoading(object oSender, EventArgs eaArgs) {
22: }
23: }
24: }
Three variables and five functions, not a very large class. Hard to say how much we'll be adding to this as we go though. let me explain what the variables will be used for.
- cglGLView will be the control on the form where our OpenGL graphics will be displayed. This does mean that we won't be in full screen mode, however using a form means we can have easy access to inputs and outputs.
- clblLog is a label that I'll be writing various debug type information to. I realize I could just leave the console showing and drop this text there, but I wanted the form to be self contained.
- cbGLReady is a flag that will let me know when OpenGL is ready for me to use. The GLControl gives us space to draw, but we can't draw on it until both the control and OpenGL are ready for us. This flag will be my indicator that it's safe to draw.
The Main() function is where the application will start in. All it needs to do is create our form and pass control to that form.
1: public static void Main() {
2: GLForm frmGL;
3:
4: //Create the form and give it control
5: frmGL = new GLForm();
6: Application.Run(frmGL);
7: }
GLForm() is the constructor of our class. It will initialize the class variables and then create and position the form's controls. It's all fairly standard Windows Forms stuff.
1: public GLForm() {
2: //Initialize class variables
3: cbGLReady = false;
4: cglGLView = new GLControl();
5: clblLog = new Label();
6:
7: //Position and configure the controls
8: cglGLView.Top = 0;
9: cglGLView.Left = 0;
10: cglGLView.Width = 640;
11: cglGLView.Height = 480;
12:
13: clblLog.Top = 485;
14: clblLog.Left = 0;
15: clblLog.Width = 800;
16: clblLog.Height = 125;
17: clblLog.Text = "";
18:
19: //Define callbacks for the controls
20: cglGLView.Load += new EventHandler(GLViewLoading);
21: cglGLView.Paint += new PaintEventHandler(GLViewPainting);
22:
23: //Position and configure the form
24: this.Width = 775;
25: this.Height = 670;
26: this.FormBorderStyle = FormBorderStyle.FixedSingle;
27: Controls.Add(cglGLView);
28: Controls.Add(clblLog);
29: }
WriteLog() is a function I created to manage the text shown in that logging label. It trims the text so that it never exceeds what can be shown in that control. It boils down to a bunch of string manipulation.
1: public int WriteLog(string strText) {
2: string strCurrText, strNewText;
3: string[] astrList;
4: int iCtr;
5:
6: strCurrText = clblLog.Text;
7: astrList = strCurrText.Split('\n');
8:
9: if (astrList.Length > 9) { //log shows too man lines, truncate
10: strCurrText = "";
11: for (iCtr = 0; iCtr < 10; iCtr++) {
12: strCurrText = astrList[astrList.Length - iCtr - 1] + "\n" + strCurrText;
13: }
14: } else { //Array has space, just add line
15: strCurrText = clblLog.Text;
16: }
17:
18: strNewText = strCurrText + "\n" + strText;
19:
20: //Remove extraneous new lines
21: strNewText = strNewText.Replace("\n\n", "\n");
22: while (strNewText.Substring(0, 1) == "\n") {
23: strNewText = strNewText.Substring(1, strNewText.Length - 2);
24: }
25:
26: //Put the log in the label
27: clblLog.Text = strNewText;
28: return 0;
29: }
Here we are, finally getting into the fun stuff. When the GLControl sends the GLViewLoading event it means that the control is ready for us to use, for us it means it's now time to configure OpenGL. The GLViewLoading() function is our handler for this event and it'll be the place where we'll setup OpenGl.
1: private void GLViewLoading(object oSender, EventArgs eaArgs) {
2: //The GLControl is now loading
3: Matrix4 pmatMatrix;
4: float fAspect;
5: WriteLog("GLControl: Loading...");
6:
7: //Setup GL
8: GL.ClearColor(0.0f, 0.0f, 0.2f, 0.0f);
9: GL.Enable(EnableCap.DepthTest);
10:
11: //Setup the viewport
12: GL.Viewport(0, 0, cglGLView.Width, cglGLView.Height);
13: fAspect = cglGLView.Width / cglGLView.Height;
14: pmatMatrix = Matrix4.CreatePerspectiveFieldOfView(MathHelper.PiOver4, fAspect, 0.1f, 100.0f);
15: GL.MatrixMode(MatrixMode.Projection);
16: GL.LoadMatrix(ref pmatMatrix);
17: GL.MatrixMode(MatrixMode.Modelview);
18:
19: //Determine GL Version
20: WriteLog("OpenGL Version: " + GL.GetString(StringName.Version));
21: cbGLReady = true;
22: }
Let's break this one down line by line, since a lot of what goes on here is fairly important.
Line 8 sets the clear color for OpenGL. When you start drawing a frame you will probably start by clearing it's current contents, filling it with a single color. This sets that color. OpenGL uses percentages of four color values when specifying colors; red, green, green and alpha. You can specify floating point values from 0 meaning none of that color, to 1 meaning all of that color. In this case I'm using no red, green, or alpha, and 20% of blue; which should clear the screen to a very dark shade of blue.
In line 9 I'm enabling the Depth Test feature. There's a number of features that OpenGL provides, but it doesn't have them enabled by default. So you'll want to inform it which ones you'd like. In this case we're turning on the Depth Test, which tells OpenGL to determine if part of a polygon should be hidden by a previously drawn object. You can read more about that feature here.
Next we need to create a viewport to define what all is visible. Line 13 determines the aspect ratio of our display, in this case it's the shape of the GLControl object. Line 14 calculates a perspective projection matrix. This is used to define the pyramidal shape that is the viewable region. In order we pass the field of view angle in radians (∏ divided by 4 radians is 45 degrees), aspect ratio of the display, distance to the near clipping plane, and distance to the far clipping plane. In line 15 we tell OpenGL to switch to projection mode, I'm not 100% sure what all the modes do but I can say that OpenGL has a number of different modes you can put it in. Each mode changes what sort of things you can tell OpenGL to do, in this case we need to be in Projection mode in order to define our perspective. Now in the proper mode line 16 gives our projection matrix to OpenGL. Finally in line 17 we return to Model mode, this is the mode that allows us to specify polygons to be drawn. I always try to return to Model mode since most of the stuff I want to do requires that mode.
In line 20 I'm just logging the version of OpenGL that's available. This actually becomes somewhat important to know. Each video card and it's associated drivers will specify which OpenGL features they support. Based on the available features the appropriate version of OpenGL will be available to you. If you have a video card by ATI or Nvidia then odds are good that you've got the latest OpenGL version to work with. One of the computers I code on has some Intel card in it and it only provides version 1.4, given that the latest is 4.4 it seems pretty old. We'll have to deal more with the version once we get into the actual drawing, for now just take note of what version you have access to.
Lastly in step 21 I'm updating the flag to say that OpenGL is ready for use. The GLControl is fully loaded and we've configured everything we need to do our drawing.
This brings us to the GLViewPainting() function, which is our handler for any painting events the GLControl sends. This event will fire anything some portion of that control is hidden from view by another window. Which isn't nearly good enough to render any sort of animation, but it will make sure our control always shows what is rendered so it's good enough for now.
Again let's take this line by line, there's not a lot in there but it'd be useful to know whats going on.
Lines 2 through 4 check our flag to see if it's safe to draw. If either the control or OpenGL aren't ready then an exception will be thrown by any attempts to draw, this avoids that problem.
On line 6 we're clearing our off-screen drawing buffer. OpenGL allows us to work with multiple buffers to render scenes on. Only one of them is shown on the screen the rest are kept in memory. We render the scenes on the buffers in memory, and then once they are complete we swap that with the buffer shown on screen. This avoids all manner of graphical glitches which can occur if you draw directly on the screen. The values passed to the GL.Clear() function define what all we want to clear. In this case we want all of the color information to go as well as any depth information. Since we have depth testing enabled we'll need to clear the depth information, otherwise we could ignore it here.
On line 7 I'm switching into Model mode again, just in case we're not already there. It'd do no good to attempt to do drawing if we're in the wrong mode. I normally will switch back to Model mode once I'm done in the other mode, but in case I forgot this should keep me safe.
Finally line 9 performs the buffer switching I mentioned above. Our frame is complete, so let's push it up onto the screen. In turn the buffer that was on the screen is put in memory for us to draw the next frame on.
Once this is compiled and run, it comes out looking something like this:
Not terribly fancy, but it does give us something to build upon as we dive into the more interesting features that OpenGL/OpenTK make available to us.
You can see the full source of this program here.
Line 8 sets the clear color for OpenGL. When you start drawing a frame you will probably start by clearing it's current contents, filling it with a single color. This sets that color. OpenGL uses percentages of four color values when specifying colors; red, green, green and alpha. You can specify floating point values from 0 meaning none of that color, to 1 meaning all of that color. In this case I'm using no red, green, or alpha, and 20% of blue; which should clear the screen to a very dark shade of blue.
In line 9 I'm enabling the Depth Test feature. There's a number of features that OpenGL provides, but it doesn't have them enabled by default. So you'll want to inform it which ones you'd like. In this case we're turning on the Depth Test, which tells OpenGL to determine if part of a polygon should be hidden by a previously drawn object. You can read more about that feature here.
Next we need to create a viewport to define what all is visible. Line 13 determines the aspect ratio of our display, in this case it's the shape of the GLControl object. Line 14 calculates a perspective projection matrix. This is used to define the pyramidal shape that is the viewable region. In order we pass the field of view angle in radians (∏ divided by 4 radians is 45 degrees), aspect ratio of the display, distance to the near clipping plane, and distance to the far clipping plane. In line 15 we tell OpenGL to switch to projection mode, I'm not 100% sure what all the modes do but I can say that OpenGL has a number of different modes you can put it in. Each mode changes what sort of things you can tell OpenGL to do, in this case we need to be in Projection mode in order to define our perspective. Now in the proper mode line 16 gives our projection matrix to OpenGL. Finally in line 17 we return to Model mode, this is the mode that allows us to specify polygons to be drawn. I always try to return to Model mode since most of the stuff I want to do requires that mode.
In line 20 I'm just logging the version of OpenGL that's available. This actually becomes somewhat important to know. Each video card and it's associated drivers will specify which OpenGL features they support. Based on the available features the appropriate version of OpenGL will be available to you. If you have a video card by ATI or Nvidia then odds are good that you've got the latest OpenGL version to work with. One of the computers I code on has some Intel card in it and it only provides version 1.4, given that the latest is 4.4 it seems pretty old. We'll have to deal more with the version once we get into the actual drawing, for now just take note of what version you have access to.
Lastly in step 21 I'm updating the flag to say that OpenGL is ready for use. The GLControl is fully loaded and we've configured everything we need to do our drawing.
This brings us to the GLViewPainting() function, which is our handler for any painting events the GLControl sends. This event will fire anything some portion of that control is hidden from view by another window. Which isn't nearly good enough to render any sort of animation, but it will make sure our control always shows what is rendered so it's good enough for now.
1: private void GLViewPainting(object oSender, EventArgs eaArgs) {
2: if (cbGLReady == false) {//Control is not loaded, do nothing
3: return;
4: }
5: //Clear the GL View
6: GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
7: GL.MatrixMode(MatrixMode.Modelview);
8: //Drawing complete
9: cglGLView.SwapBuffers();
10: }
Again let's take this line by line, there's not a lot in there but it'd be useful to know whats going on.
Lines 2 through 4 check our flag to see if it's safe to draw. If either the control or OpenGL aren't ready then an exception will be thrown by any attempts to draw, this avoids that problem.
On line 6 we're clearing our off-screen drawing buffer. OpenGL allows us to work with multiple buffers to render scenes on. Only one of them is shown on the screen the rest are kept in memory. We render the scenes on the buffers in memory, and then once they are complete we swap that with the buffer shown on screen. This avoids all manner of graphical glitches which can occur if you draw directly on the screen. The values passed to the GL.Clear() function define what all we want to clear. In this case we want all of the color information to go as well as any depth information. Since we have depth testing enabled we'll need to clear the depth information, otherwise we could ignore it here.
On line 7 I'm switching into Model mode again, just in case we're not already there. It'd do no good to attempt to do drawing if we're in the wrong mode. I normally will switch back to Model mode once I'm done in the other mode, but in case I forgot this should keep me safe.
Finally line 9 performs the buffer switching I mentioned above. Our frame is complete, so let's push it up onto the screen. In turn the buffer that was on the screen is put in memory for us to draw the next frame on.
Once this is compiled and run, it comes out looking something like this:
Not terribly fancy, but it does give us something to build upon as we dive into the more interesting features that OpenGL/OpenTK make available to us.
You can see the full source of this program here.
No comments:
Post a Comment