C# wrapper for OpenGL
for the Windows operating system
Colin Fahey

GRControl on a form, showing a "brick" pixel shader program and GDI+ drawing copied to a texture

Two GRControl instances on a Form, showing pixel shader programs and textures

A 3D avatar that roams the desktop and can be dragged around
1. Software
GRV11_20080809.zip
C# wrapper for OpenGL : version 11
3335388 bytes
MD5: 36a2124df60d3e7069e747b0ebf9f58d
2. All of the computer code is in the "public domain"
I wrote all computer code within the "C# wrapper for OpenGL".
I declare all computer code within the "C# wrapper for OpenGL" to be in the "public domain".
Therefore, the computer code can be used for any purpose, commercial or private, without paying me and without mentioning my name.
The computer code can be modified without restriction and without obligations.
The example programs include a small amount of computer code (specifically, "shader" computer code) written by other people. Such code is not within the wrapper itself and can be trivially removed. Such code is merely to demonstrate the simplicity of using the wrapper to create and use "shader" programs.
3. New Features
Example programs show how to do GDI+ drawing to a Bitmap, and then copy that Bitmap to a texture.
This enables text and other GDI+ drawing capabilities to contribute to an OpenGL scene.
The GDI+ Bitmap can be updated and uploaded to an OpenGL texture as often as once per OpenGL frame.
Example programs show how to choose from among several different shader programs while the program is running.
Example programs show how to save OpenGL buffers to JPG, BMP, GIF, and PNG files.
(Press Shift + 0 (shift-zero) to write the OpenGL buffer to image files.)
Example project shows how the wrapper can be compiled as a DLL (GRV11DLL.dll); and such a DLL can eliminate the insane IntelliSense slowdowns that affects editing in projects that include GRV11.cs among its source files.
GRExample2 now shows the two GRControl instances in a split-view control.
This allows the two controls to better adjust to a resizing Form.
This also allows a person to decide how to split the available space across the two controls.
An "avatar" example shows how to make a 3D avatar that roams the desktop.
4. Introduction
"GRV11.cs" is a C# source code file that implements a complete wrapper for OpenGL, including all OpenGL extensions (as of 2007).
This single C# source code file implements a class named "GRControl" derived from "System.Windows.Forms.Control".
It is easy to use the "GRControl" class to add one or more OpenGL windows to any Windows Form (software window).
5. Details
* The control is only for Windows operating systems that support the .NET 2.0 Framework (Windows 98, Windows 2000, Windows XP, Windows Vista)
* Requires .NET 2.0 to compile, and requires .NET 2.0 redistributables (run-time libraries) to be installed on the computer;
* Requires "/unsafe" compile option when compiling with "GRV11.cs".
( The "/unsafe" option is not required if the software refers to a precompiled, external DLL, "GRV11DLL.dll", built from "GRV11.cs".
In this scenario, "GRV11.cs" is not part of the project, whereas "GRV11DLL.dll" is added as a "reference". )
* The control can be compiled and used by any of the following:
Mono Project ( >= Mono 1.2.4)
Microsoft .NET 2.0 SDK
SharpDevelop 2.2
Microsoft Visual C# 2005 Express Edition
Microsoft Visual Studio 2005
* Offers most OpenGL extensions defined (as of 2007), including vertex shaders and fragment (pixel) shaders;
* 1570 OpenGL-related functions:
336 GL functions
51 GLU functions
19 WGL functions
1164 extensions
( Somewhat more than 1570 functions are made available by the wrapper to enable calling the functions with different parameter types. )
* Over 3244 defined constants for OpenGL and extensions;
* Allows a Form to have multiple OpenGL rendering contexts, all animating independently;
* Clicking on on the control gives it focus, allowing subsequent keyboard and mouse wheel input; (mouse clicks and movements are received when the mouse cursor is within the control's rectangular area);
* The software module offers some support code to demonstrate how to do some common OpenGL tasks in C# and .NET 2.0;
* All OpenGL constants and functions appear in alphabetical order in the GR class within the GRV11.cs source file, making it easy to determine if any constant or function is missing (which is unlikely, except for extensions adopted after early 2007)
6. Using the control
There are two methods of using GRV11.GRControl in a new project:
(1) Adding the source file "GRV11.cs" to a project;
(2) Adding a pre-compiled "GRV11DLL.dll" to the "References" in a project.
Adding the source file "GRV11.cs" to a project
Advantage: The code can be easily examined and possibly modified.
Advantage: The final executable does not need GRV11DLL.dll to execute.
Disadvantage: In Microsoft Visual C# Express Edition, or in Microsoft Visual Studio, the large number of classes, functions, and variables in "GRV11.cs" makes the IntelliSense feature stop the editor window for 15 seconds or more, several times each minute.
This makes editing code a nightmare in this editor.
The SharpDevelop 2.2 editor is a replacement for the Microsoft editor.
( Adding GRV11DLL.dll as a reference, instead of having GRV11.cs source code in the project, will allow IntelliSense to work in the Microsoft editors without any slowdown at all. )
Disadvantage: "GRV11.cs" must be compiled with the "/unsafe" compiler option.
It is inconvenient, but relatively easy, to set this option in the build settings.
( Adding GRV11DLL.dll as a reference, instead of having GRV11.cs source code in the project, will allow the project to compile without using the "/unsafe" option. )
Adding a pre-compiled "GRV11DLL.dll" to the "References" in a project
Disadvantage: The source code is not readily available when editing.
Disadvantage: The GRV11DLL.dll must be in the same directory as the executable file (*.exe) of any software that uses that DLL.
Thus, such software is not "self-contained".
Note that it might be possible to incorporate the GRV11DLL.dll in to the final executable.
You can attempt to do that.
However, you can also simply compile using the source code, GRV11.cs, when you are satisfied that your software is ready for release.
Advantage: IntelliSense in the Microsoft Visual C# and Microsoft Visual Studio editors will be very fast.
( Having "GRV11.cs" source code in a project makes those editors unusable for many long periods! )
Advantage: The software can be compiled without the need for the "/unsafe" compiler option.
This makes it even easier to simply create a new project, add the reference to GRV11DLL.dll, and immediately compile without adjusting compiler options.
Setting a build option is relatively easy, but anything that eliminates a step in the process makes it that much more appealing for everyone.
7. Adding an instance of GRControl to a System.Windows.Forms.Form
If you are using a development environment such as Microsoft Visual Studio 2005, Microsoft Visual C# 2005 Express Edition, or SharpDevelop2, then adding a Form to a project will generate several source code files, with names such as the following:
Form1.cs
Form1.Designer.cs
Form1.resx
The file "Form1.cs" might be obvious in the Solution Explorer, but you might have to right-click and select "View Code" to edit the source code.
Also, the files "Form1.Designer.cs" and "Form1.resx" might be hidden in child tree nodes in the Solution Explorer tree view.
Click on the [+] in front of the "Form1.cs" tree node to expose these files.
These files are normally only modified by the development environment automatically whenever the programmer is changing the properties of a form in the "Designer" view of a Form.
If you right-click on "Form1.cs" in the Solution Explorer tree view, you will see the option to "rename" the file.
If you change the name, you might be invited to allow the development environment to change the names of the corresponding "designer" and "resx" files.
Suppose that you rename "Form1.cs" to "GRExampleForm1.cs", and accept the name changes to the related files, resulting in the following new file names:
GRExampleForm1.cs
GRExampleForm1.Designer.cs
GRExampleForm1.resx
The following is the complete source code for "GRExampleForm1.cs", with a single instance of GRControl, and setting up event handlers for the various events that the GRControl can generate.
Notice that this example uses a timer to repeatedly tell the GRControl to repaint itself.
This is suitable for continuous animation.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using GRV11; // Requires GRV11.cs source code or a 'reference' to GRV11DLL.dll
public partial class GRExampleForm1 : Form
{
public GRExampleForm1()
{
InitializeComponent();
this.mGRExampleHandler1 = new GRExampleHandler1();
this.mGRControl.OpenGLStarted += new GRControl.DelegateOpenGLStarted(this.mGRExampleHandler1.OpenGLStarted);
this.mGRControl.KeyDown += new KeyEventHandler(this.mGRExampleHandler1.KeyDown);
this.mGRControl.KeyUp += new KeyEventHandler(this.mGRExampleHandler1.KeyUp);
this.mGRControl.MouseDown += new MouseEventHandler(this.mGRExampleHandler1.MouseDown);
this.mGRControl.MouseUp += new MouseEventHandler(this.mGRExampleHandler1.MouseUp);
this.mGRControl.MouseMove += new MouseEventHandler(this.mGRExampleHandler1.MouseMove);
this.mGRControl.MouseWheel += new MouseEventHandler(this.mGRExampleHandler1.MouseWheel);
this.mGRControl.Paint += new PaintEventHandler(this.mGRExampleHandler1.Paint);
this.mTimer = new System.Windows.Forms.Timer();
this.mTimer.Interval = 10; // 10-millisecond timer
this.mTimer.Tick += new EventHandler(PrivateTimerTickEventHandler);
this.mTimer.Start();
// Set focus to the control so that it can immediately accept input
this.mGRControl.Focus();
// Also, whenever the form becomes activated, set focus to the main
// control on the form. The following sets up an event handler for
// that purpose.
this.Activated += new EventHandler(PrivateActivatedEventHandler);
// We want to preview dialog keys (most importantly, the cursor
// keys: up, down, right, left) so we can forward such events to
// the appropriate child control.
this.KeyPreview = true;
}
public GRControl mGRControl;
public GRExampleHandler1 mGRExampleHandler1;
private System.Windows.Forms.Timer mTimer;
void PrivateTimerTickEventHandler(object sender, EventArgs e)
{
if (false == DesignMode)
{
this.mGRControl.Invalidate();
}
}
private void PrivateActivatedEventHandler(object sender, EventArgs e)
{
// When this form becomes activated, after some time of not
// being active, set input focus to the main control on the form.
this.mGRControl.Focus();
}
// Cursor keys (up,down,left,right) need to be specially captured
// and forwarded to the control.
// CAUTION: The KeyPreview property of this Form must be set to 'true'
// for the following method to be invoked.
protected override bool ProcessDialogKey(Keys keyData)
{
if
(
(keyData == Keys.Up)
¦¦ (keyData == Keys.Down)
¦¦ (keyData == Keys.Left)
¦¦ (keyData == Keys.Right)
)
{
KeyEventArgs e = new KeyEventArgs(keyData);
this.mGRExampleHandler1.KeyDown(this.mGRControl, e);
return (true);
}
return base.ProcessDialogKey(keyData);
}
}
It is necessary to manually edit the code within "GRExampleForm1.Designer.cs" (the designer part of our example Form) to add the control to the Form in such a manner that it can be subsequently seen in the "designer" view of the Form.
Once this code is modified as shown, the control will appear in the "designer" view, and the control can be graphically moved around and have its properties changed in the properties window.
It requires some care to avoid making a mistake when manually editing any "*.Designer.cs" file, but there is not much to add to get things started.
( Note: You might have to click on a [+] node to reveal the "InitializeComponent()" code.
The development environment is discouraging you, yet again, from editing this code.
Simply proceed, taking care to follow the pattern of other code in the InitializeComponent() method. )
partial class GRExampleForm1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.mGRControl = new GRV11.GRControl();
this.SuspendLayout();
//
// mGRControl
//
this.mGRControl.Anchor = ((System.Windows.Forms.AnchorStyles)
((((System.Windows.Forms.AnchorStyles.Top
¦ System.Windows.Forms.AnchorStyles.Bottom)
¦ System.Windows.Forms.AnchorStyles.Left)
¦ System.Windows.Forms.AnchorStyles.Right)));
this.mGRControl.Location = new System.Drawing.Point(12, 12);
this.mGRControl.Name = "GRControl";
this.mGRControl.Size = new System.Drawing.Size(608, 422);
this.mGRControl.TabIndex = 0;
this.mGRControl.TabStop = false;
this.mGRControl.Text = "GRControl #1";
//
// GRExampleForm1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(632, 446);
this.Controls.Add(this.mGRControl);
this.Name = "GRExampleForm1";
this.Text = "GRExampleForm1";
this.ResumeLayout(false);
}
#endregion
}
If you add "GRV11.cs" to a new project, then GRControl might appear in the "Toolbox" alongside all of the other Control choices (such as "Button" and "PictureBox").
This would enable you to drag-and-drop the control to new Forms.
However, editing the "Designer" code file often will be necessary to initially add the control to a Form.
8. Handling events invoked by an instance of GRControl
The GRControl control will initialize OpenGL, and will receive input events (mouse clicks, keyboard presses, mouse wheel events).
The programmer can append event handlers to the GRControl event properties to receive events from the GRControl.
Here is a list of interesting events:
OpenGLStarted GRControl.DelegateOpenGLStarted
Paint System.Windows.Forms.PaintEventHandler
KeyDown System.Windows.Forms.KeyEventHandler
KeyUp System.Windows.Forms.KeyEventHandler
MouseDown System.Windows.Forms.MouseEventHandler
MouseUp System.Windows.Forms.MouseEventHandler
MouseMove System.Windows.Forms.MouseEventHandler
MouseWheel System.Windows.Forms.MouseEventHandler
Only the "OpenGLStarted" event is an event type specific to the GRControl.
All other events are standard System.Windows.Forms events, whose event handlers must have certain parameters (specified in any Forms documentation).
The "OpenGLStarted" and "Paint" events are the most important events for the control.
The "OpenGLStarted" event is invoked before the very first "Paint" event is invoked.
Therefore, the "OpenGLStarted" event handler must be added to the control before the control has an opportunity to attempt to paint itself, otherwise the one-time event will not be received.
This event is merely a convenience, and can be ignored.
However, adding a handler for this event makes it possible for the handler code to do some one-time initialization that depends on OpenGL being ready to use.
( OpenGL cannot be used until the window exists and the window is ready to perform its first paint.
So, the "OpenGLStarted" event spares the programmer from the risk of attempting to use OpenGL too soon after the program starts.
) It is fairly easy to add logic in a "Paint" handler to do a one-time initialization, using the OpenGL context of the GRControl.
The "OpenGLStarted" event is offered as a convenience.
A handler for the "OpenGLStarted" event has the following structure:
public void OpenGLStarted( GRControl grControl )
{
GR gr = grControl.GetGR();
if (null == gr) return;
// It is now OK to use OpenGL methods (via "gr").
// . . .
}
The "Paint" event handler (or functions invoked by the "Paint" event handler) should be the only place in which OpenGL drawing commands are invoked.
The "Paint" event handler has the following structure:
public void Paint ( object sender, PaintEventArgs e )
{
if (null == sender) return;
if (false == (sender is GRControl)) return;
GRControl grControl = (sender as GRControl);
GR gr = grControl.GetGR( );
int clientWidth = grControl.ClientRectangle.Width;
int clientHeight = grControl.ClientRectangle.Height;
if (clientWidth <= 0)
{
clientWidth = 1;
}
if (clientHeight <= 0)
{
clientHeight = 1;
}
// Viewport
gr.glViewport( 0, 0, clientWidth, clientHeight );
// Clear the viewport
gr.glClearColor( 0.5f, 0.5f, 0.5f, 0.0f );
gr.glClear( GR.GL_COLOR_BUFFER_BIT ¦ GR.GL_DEPTH_BUFFER_BIT );
// **** ALL DRAWING OF A FRAME GOES HERE ****
// Flush all the current rendering and flip the back buffer to the front.
gr.wglSwapBuffers( grControl.GetHDC() );
}
Please refer to the sample programs to learn more about how such drawing can work.
All OpenGL functions are invoked using an instance of the "GR" class (example: gr.gl*()).
All OpenGL constants are used by specifying the class name (example: GR.GL_*).
9. Checking for the availability of an extension function of OpenGL
Simply check if the Boolean flag of the same name as the OpenGL function is 'true' before attempting to use the corresponding function.
The Boolean variable has a name that begins with 'b' followed by the OpenGL function name.
if (true == gr.bglCreateProgramObjectARB)
{
// It's safe to invoke glCreateProgramObjectARB()
// ...and it's probably okay to invoke any core shader-related
// function, since they're all essential to use shaders.
}
Checking is only needed for extension functions.
All version 1.1 OpenGL functions, and all GLU functions, and all WGL functions, do not require such checking.
Knowing whether or not a function is an extension is the programmer's responsibility.
However, function name suffixes, such as "EXT", "ARB", "MESA", "NV" (NVidia), "APPLE", etc, help indicate extension functions.
If a function "gl*()" has a neighboring function "gl*ARB()" or "gl*NV()", then the function "gl*()" is probably an extension (as far as the Windows OPENGL32.DLL is concerned).
The only advantage of knowing whether or not a function is an "extension" (with respect to the Windows OPENGL32.DLL) is being able to avoid checking if functions are available.
You can search for all OpenGL functions used in your code, and then build an alphabetical list of the different functions used.
Then, at the beginning of your program, after the GRControl calls the OpenGLStarted delegate, simply check all Boolean flags for all the functions you intend to use.
If you choose to terminate the program if any "required" functions are missing, then all checks for such functions in the code can be eliminated.
Also, you can form a group of non-essential functions in the check, and possibly inform the user of any feature changes, and possibly leave checks scattered around in the code.
With more OpenGL experience, you can get a sense of what extensions are related (examples: imaging, shaders, buffers, compression, ...).
Therefore, you can check for the presence of a single critical function and use that information to decide if the whole subset is likely to be present.
10. Example programs
GRExample1 features a single GRControl on a Form (which I named GRExampleForm1), and the example handler used in conjunction with the control demonstrates the use of several pixel shaders.
This demonstration program is used to demonstrate the use of the GRControl for multiple compilers (Mono 1.2.4, MS .NET SDK, MSVC# 2005 Express Edition, SharpDevelop 2.2).
GRExample2 features two GRControls on a Form (which I named GRExampleForm2), and uses two distinct sets of handler functions.
One of the sets of handler functions is the same as in the GRExample1 (showing a cube drawn using a pixel shader), while the other set of handler functions shows a textured cube.
If a file named "image.jpg" is in the directory with the executable (if executing directly), or in the source code directory (if executing from within the IDE without overriding the execution "working directory"), then the image will appear on the cube.
Otherwise, a generated checkerboard texture will be put on the cube.
"Avatar" features a GRControl and a specially-initialized Form to create a 3D avatar that roams the desktop.
Double-click the avatar to close the program (or press the avatar once to select it (it is a window), and then either press Esc or Alt+F4 to close it).
You can drag the avatar to a new position on the desktop.
You can click on the avatar and then use the arrow cursor keys to "drive" the avatar around the desktop.
You can interact with the avatar with the keyboard in another small way: you can press the 'A' and 'Z' keys to make move the "camera", making the 3D model bigger or smaller -- but keep in mind that the avatar window is a fixed size.
( Just for fun, I launched five of the Avatar programs.
The speed was relatively slow, but it was fun to see the five cubes roaming around my desktop while I worked on other things. )
11. Speed
For various reasons, C# is slower than non-CLR C/C++.
C# is eventually compiled down to native assembly language, just as for C/C++, but between the assurances made by the C# language and the .NET CLR, the overall speed of software built upon C# and .NET is somewhat slower than a non-CLR C/C++ counterpart.
Therefore, getting the highest performance possible means using non-CLR C/C++ instead of C#.
Furthermore, because calling any native library from C# involves P/Invoke, invoking functions such as the OpenGL functions will require some time to perform work in the P/Invoke layer.
Nonetheless, my C# wrapper appears to work well enough to be used for many real-time graphical purposes, such as simple games, 3D viewers, editors, or 3D presentations. This is especially true if most of the work is being done by the GPU instead of the CPU.
12. Compare with the Tao Framework
The Tao Framework is a massive C# / .NET wrapper for many open-source libraries, such as OpenGL, OpenAL (audio), SDL (a whole gaming/simulation platform), Open Dynamics Engine (ODE), etc.
The Tao Framework is cross-platform (Windows, Linux, Mac OSX).
My own C# wrapper of OpenGL is a single source code file, and provides a very simple interface to all OpenGL functions and extensions.
My wrapper is only designed to work on the Windows operating system, however it might work under Linux with WINE.
The Tao Framework has a community, and one might benefit from communicating with fellow Tao Framework users.
However, I believe that using my code would be a better choice than the Tao Framework for many OpenGL applications.
Using my code is as easy as adding a single, pure-C# source file, or a DLL built from that source, to one's project.
Also, my code is exposed as a Forms Control, making it easy to add multiple instances to a Form.
Also, I provide Boolean variables to indicate the availability of any function, for rapid checking at run time.
13. .NET compiler and end-user libraries
I have written notes about installing a compiler, or the end-user run-time libraries, for the .NET platform.
Note that the software distribution that Microsoft is currently calling ".NET 3.0" (as of 2007 February) is the unmodified ".NET 2.0 Framework" (released in 2005, and largely implemented in Mono) plus entirely new, specialized libraries (examples: "WPF", "WF") unrelated to the C# compiler or Framework Class Library (FCL).
The ".NET 3.0" distribution uses the unmodified ".NET 2.0 SDK" installer, and then installs the unrelated, new, specialized libraries mentioned above.