Introduction
I had recently written some code to display lots of simple 2D shapes for a finite element program. My first thought was to use GDI. However, since I had to display thousands of shapes on the screen, GDI was too slow for me. The solution came up as an OpenGL control. In this article, I will try to explain how I created a control utilizing OpenGL for a 2D shape drawing.
Using the Code
The source code includes a control named GLCanvas2D based on System.Windows.Forms.UserControl. OpenGL specific initialization code is as follows:
GLCanvas2D::GLCanvas2D()
{
mhDC = GetDC((HWND)this->Handle.ToPointer());
PIXELFORMATDESCRIPTOR pfd = {
sizeof(PIXELFORMATDESCRIPTOR), 1, PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
PFD_TYPE_RGBA, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 24, 0, PFD_MAIN_PLANE, 0, 0, 0, 0 };
int iPixelFormat = ChoosePixelFormat(mhDC, &pfd);
SetPixelFormat(mhDC, iPixelFormat, &pfd);
mhGLRC = wglCreateContext(mhDC);
wglMakeCurrent(mhDC, mhGLRC);
}
This code is called in the constructor of GLCanvas2D. The destructor deletes the render context and releases the device context.
GLCanvas2D::~GLCanvas2D()
{
wglMakeCurrent(NULL, NULL);
wglDeleteContext(mhGLRC);
ReleaseDC((HWND)this->Handle.ToPointer(), mhDC);
}
I have two variables used for panning and zooming the view: a point structure containing a camera position and a zoom factor. In each paint event, the projection matrix is setup using these two variables.
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(mCameraPosition.X -
((float)ClientRectangle.Width) * mZoomFactor / 2, mCameraPosition.X +
((float)ClientRectangle.Width) * mZoomFactor / 2, mCameraPosition.Y -
((float)ClientRectangle.Height) * mZoomFactor / 2, mCameraPosition.Y +
((float)ClientRectangle.Height) * mZoomFactor / 2, -1.0f, 1.0f);
Camera position and zoom factor are calculated when the user pans (by holding down the mouse wheel) or zooms (by scrolling the mouse wheel). I also needed some method of transforming coordinates from screen to model coordinates and back.
Drawing::Point WorldToScreen(float x, float y)
{
return Drawing::Point(
(int)((x - mCameraPosition.X) / mZoomFactor) +
ClientRectangle.Width / 2,
-(int)((y - mCameraPosition.Y) / mZoomFactor) +
ClientRectangle.Height / 2);
}
Drawing::PointF ScreenToWorld(int x, int y)
{
return Drawing::PointF(
(float)(x - ClientRectangle.Width / 2) * mZoomFactor +
mCameraPosition.X,
-(float)(y - ClientRectangle.Height / 2) * mZoomFactor +
mCameraPosition.Y);
}
GLCanvas2D exposes a Render event which is typically overridden in implementations. The control overrides the OnPaint event of the base class and raises the Render event passing a GLGraphics object as an argument. GLGraphics is similar to the System.Drawing.Graphics class. Drawing is done using the methods of the GLGraphics object.
void MyCanvas2D::Render(System::Object ^ sender, GLView::GLGraphics ^ Graphics)
{
Graphics->FillRectangle(0.0f, 0.0f, 100.0f, 50.0f, Drawing::Color::Red);
}
Implementations can also use native OpenGL calls within the Render event.
void MyCanvas2D::Render(System::Object ^ sender, GLView::GLGraphics ^ Graphics)
{
glBegin(GL_QUADS);
glColor3f(1.0f, 0.0f, 0.0f);
glVertex2f(0.0f, 0.0f);
glVertex2f(100.0f, 0.0f);
glVertex2f(100.0f, 50.0f);
glVertex2f(0.0f, 50.0f);
glEnd();
}
Points of Interest
If an implementation places more than one instance of GLCanvas2D on a Form, we have to make sure that OpenGL calls are directed to the correct render context. We achieve this by calling wglMakeCurrent with the handle of our render context each time the OnPaint event is fired.
void GLCanvas2D::OnPaint(System::Windows::Forms::PaintEventArgs^ e)
{
HDC mhOldDC = wglGetCurrentDC();
HGLRC mhOldGLRC = wglGetCurrentContext();
wglMakeCurrent(mhDC, mhGLRC);
wglMakeCurrent(mhOldDC, mhOldGLRC);
}
GLView uses OpenGL vertex arrays to speed up drawing. Vertex arrays are handled internally by a custom GLVertexArray class. The GLGraphics class collects vertex information using two GLVertexArray's. One GLVertexArray is used for filled shapes. Each filled shape is converted to triangles and stored in the vertex array. A second GLVertexArray collects lines. No drawing is actually performed until the Render event ends.
System::Void Render()
{
float * vp = new float[mVertices->Count * 3];
float * cp = new float[mVertices->Count * 4];
for (int j = 0; j < mVertices->Count; j++)
{
vp[j * 3] = mVertices[j].x;
vp[j * 3 + 1] = mVertices[j].y;
vp[j * 3 + 2] = mVertices[j].z;
cp[j * 4] = mVertices[j].r;
cp[j * 4 + 1] = mVertices[j].g;
cp[j * 4 + 2] = mVertices[j].b;
cp[j * 4 + 3] = mVertices[j].a;
}
glVertexPointer(3, GL_FLOAT, 3 * sizeof(float), vp);
glColorPointer(4, GL_FLOAT, 4 * sizeof(float), cp);
glDrawArrays(mType, 0, mVertices->Count);
delete[] vp;
vp = 0;
delete[] cp;
cp = 0;
}
History
- 25.01.2007 - First release
- 26.04.2007 - Various bug fixes (Thanks to Jac for bug reports and suggestions)
- 05.02.2010 - Fixed a memory leak in
GLGraphics::Render