Win32 OpenGL Framework - Star Wars scrolling Text






4.38/5 (6 votes)
Aug 7, 2002
4 min read

148799

5580
An article detailing a project which contains StarWars-esque scrolling text. It is done completely using OpenGL and shows some advanced OpenGL topics. Also included is a framework for using OpenGL windows in your Win32 programs.
Motivation
I saw Pablo van der Meer's article detailing his CStarWarsCtrl. I thought it was very interesting but I didn't like the facts that it used -
- MFC and
StretchBlt
.
So I set upon the task to reimplement it using OpenGL while at the same time making it friendly for Win32.
What is it?
This article provides a Win32 OpenGL framework. It makes it easy for you by hiding most of the OpenGL initialization/shutdown code. The article shows how to use the framework to create a StarWars type effect of scrolling text.
Win32 OpenGL
The hardest part was setting up OpenGL under Win32. First you need to create a window class using the CS_OWNDC
style, and query for a pixel format. Once you have those, you can create an OpenGL rendering context and attach it to the DC of the window. After a little documentation, I found out it wasn't that difficult at all. I often make Win32 controls which do a lot of work for you. So here I stuck with my plan. I made a Win32 control that does all the OpenGL work for you. All you have to give the control is a COpenGLWndController
class and a requested pixel format and the control makes use of it. Let's take a look at it.
class COpenGLWndController { private: // these are friends because these functions need to call SetParameters friend static LRESULT OnOpenGLSetController(HWND hWnd,void *pController); friend static LRESULT OnOpenGLCreate(HWND hWnd,LPCREATESTRUCT lpcs); void SetParameters(HDC hdc,HGLRC hglrc); virtual void vDraw() = 0; // render it now HDC m_hdc; HGLRC m_hglrc; public: void Draw(); virtual ~COpenGLWndController() {;} virtual int ValidatePixelFormat(HDC hdc,int suggestedFormat); virtual void WindowSized(int cx,int cy) = 0; virtual void Init() = 0; // initialize textures virtual void Close() = 0; // the window is closing, destroy textures/etc };
Creation
To create an OpenGL window, use this function:
BOOL RegisterOpenGLWindow(HINSTANCE hInst);
// Remember, once created, the window will call 'delete' on the controller.
HWND CreateOpenGLWindow(HINSTANCE hInst,HWND hParent,
DWORD style,UINT id,LPRECT rt,
COpenGLWndController *pController,
LPPIXELFORMATDESCRIPTOR pfd);
One simply has to create a subclass of COpenGLWndController
and implement WindowSized
, vDraw
, Init
, and
Close
. WindowSized
is called in response to a WM_SIZE
message, and this is where you change your OpenGL viewport. vDraw
is the function which renders the scene. Don't get this confused with Draw
. Draw
is the public function you call to repaint the window - it handles behind the scenes things like swapping the buffers. Draw
ends up calling vDraw
anyways. Init
is called when the OpenGL window has created its rendering context and is now ready for us. You can now load your textures or initialize OpenGL as you see fit. Close
is similar, here you can delete any OpenGL textures/objects etc. ValidatePixelFormat
does not need to be overridden, but it can be. You can use this function to fiddle with the pixel format, returning a new one if you want. I use it in my implementation to turn on FSAA (full screen antialiasing).
Implementation
Let's take a look at how our subclass works - CStarWarsController
. The code for WindowSized
is pretty self explanatory.
void CStarWarsController::WindowSized(int cx,int cy) { glMatrixMode (GL_PROJECTION); glLoadIdentity(); gluPerspective(60.0,(float)cx/(float)cy,1.0,90.0); glViewport (0, 0, cx, cy); }
The code for Init
initializes the fonts used.
void CStarWarsController::Init() { HFONT hOld; HFONT hFont = CreateFont(12, 0, 0, 0, FW_NORMAL, FALSE, FALSE, 0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH, _T("Arial")); HDC hdc = wglGetCurrentDC(); hOld = (HFONT)SelectObject(hdc,hFont); wglUseFontOutlines(hdc, 0, MAX_TEXT, 1000, 0.0f, 0.1f,WGL_FONT_POLYGONS, m_agmf); SelectObject(hdc,hOld); DeleteObject(hFont); }
The code for Close
cleans up the fonts used and deletes our CObject
s
void CStarWarsController::Close() { glDeleteLists(1000,MAX_TEXT); // delete our objects now for (int i=0;i<NUMOBJECTS;++i) { if (pObjects[i]) { delete pObjects[i]; pObjects[i] = NULL; } } }
I mentioned the CObject
class. I use this class in the controller as it represents a moving object along the screen. Each line of text is treated as an object. Each object has a starting point, a vector it moves along, and a current position. Thus, for any time t
, I can calculate the current position from the starting point and movement vector. CObject
has one overriddable function, Draw()
. I provide two subclasses of CObject
: CTextObject
and CTexturedQuad
. The moving flag is a CTexturedQuad
.
The time offset might need some explaining. The objects are in an array. The first object needs to be followed by the others to look good. Each object has the same starting point. For this example, it's 0,-4,0. But each object has a time offset, for when they should appear. At 0 time, they'll appear at 0,-4,0. At a time offset of 2, they'll be closer to the viewer, because its 2 seconds behind. Therefore all objects in the array have an increasing time offset. The text objects typically require a 2 second time offset between them. This is how the objects are spaced apart. This means you can space them apart as far as you want by changing the time offset field.
typedef struct _tagVECTOR { float x; float y; float z; } VECTOR,*LPVECTOR; typedef struct _tagTDPOINT { float x; float y; float z; } TDPOINT,*LPTDPOINT; class CObject { public: CObject() ; virtual ~CObject() {;} virtual void Draw() = 0; float m_fAngle; float m_fTimeOffset; float m_fColor[3]; TDPOINT m_start; VECTOR m_slope; TDPOINT m_curPos; };
In my example, it renders constantly. CStarWarsController
has a function called Idle
which moves all the objects and stars around the screen. The code is easy, simple vector math.
void CStarWarsController::Idle() { LARGE_INTEGER now; // get current time QueryPerformanceCounter(&now); m_fTimeElapsed = ((float)(now.QuadPart - m_start.QuadPart) /(float)m_freq.QuadPart); // move the objects for (int i=0;<NUMOBJECTS;++i) { pObjects[i]->m_curPos.x = pObjects[i]->m_start.x; pObjects[i]->m_curPos.y = pObjects[i]->m_start.y + pObjects[i]->m_slope.y * (m_fTimeElapsed - pObjects[i]->m_fTimeOffset); pObjects[i]->m_curPos.z = pObjects[i]->m_start.z + pObjects[i]->m_slope.z * (m_fTimeElapsed - pObjects[i]->m_fTimeOffset); } // move the stars, calculate new time based on star m_start time m_fTimeElapsed = ((float)(now.QuadPart - m_starStart.QuadPart)/(float)m_freq.QuadPart); for (int i=0;i<m_iNumStars;++i) { // update their z position m_pStars[i].m_curPos[2] = m_pStars[i].m_start.z + m_pStars[i].speed.z * (m_fTimeElapsed - m_pStars[i].timeOffset); // ok they're out of view, respawn a new star if (m_pStars[i].m_curPos[2] >= EYE_Z) { m_pStars[i].m_start.x = GetRandom(-5.0,5.0); m_pStars[i].m_start.y = GetRandom(-5.0,5.0); m_pStars[i].m_start.z = -10.0f; m_pStars[i].timeOffset = m_fTimeElapsed; } else { m_pStars[i].m_curPos[0] = m_pStars[i].m_start.x; m_pStars[i].m_curPos[1] = m_pStars[i].m_start.y; } } }
Similarly, the vDraw
function doesn't do much besides rendering the stars and calling CObject::Draw
.
/* Method to actually draw on the control */ void CStarWarsController::vDraw() { glClearColor(0.0,0.0,0.0,0.0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); if (!m_bStarted) return; glHint(GL_MULTISAMPLE_FILTER_HINT_NV,GL_NICEST); glEnable(GL_MULTISAMPLE_ARB); glDisable(GL_BLEND); glCullFace(GL_BACK); glEnable(GL_CULL_FACE); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(0.0,0.0,EYE_Z,0.0,0.0,0.0,0.0,1.0,0.0); // now draw stars - as points if (m_bPointStars) { glBegin(GL_POINTS); for (int i=0;i<m_iNumStars;++i) { glColor3fv(m_pStars[i].m_fColor); glVertex3fv(m_pStars[i].m_curPos); } glEnd(); } else // draw stars as quads { glBegin(GL_QUADS); for (int i=0;i<m_iNumStars;++i) { #define LENGTH 0.02f glColor3fv(m_pStars[i].m_fColor); glVertex3f(m_pStars[i].m_curPos[0]- LENGTH,m_pStars[i].m_curPos[1]-LENGTH, m_pStars[i].m_curPos[2]); glVertex3f(m_pStars[i].m_curPos[0]-LENGTH, m_pStars[i].m_curPos[1]+LENGTH, m_pStars[i].m_curPos[2]); glVertex3f(m_pStars[i].m_curPos[0]+LENGTH, m_pStars[i].m_curPos[1]+LENGTH, m_pStars[i].m_curPos[2]); glVertex3f(m_pStars[i].m_curPos[0]+LENGTH, m_pStars[i].m_curPos[1]-LENGTH, m_pStars[i].m_curPos[2]); } glEnd(); } // now draw text glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_BLEND); float distance,alpha; for (int i=0;i<NUMOBJECTS;++i) { if (!pObjects[i]) continue; // determine distance from us distance = sqrtf(pObjects[i]->m_curPos.x*pObjects[i]->m_curPos.x + pObjects[i]->m_curPos.y*pObjects[i]->m_curPos.y + pObjects[i]->m_curPos.z*pObjects[i]->m_curPos.z); // approximate the alpha value based on the distance away from us alpha = 3.75f - sqrtf(distance); if (alpha > 1.0f) alpha = 1.0f; else if (alpha < 0.0) alpha = 0.0; glPushMatrix(); // move everything into position glScalef(0.50f,0.50f,0.50f); glTranslatef(pObjects[i]->m_curPos.x, pObjects[i]->m_curPos.y,pObjects[i]->m_curPos.z); glRotatef(pObjects[i]->m_fAngle,1.0,0.0,0.0); glColor4f(pObjects[i]->m_fColor[0], pObjects[i]->m_fColor[1], pObjects[i]->m_fColor[2],alpha); pObjects[i]->Draw(); glPopMatrix(); } // ok now we check the last alpha value, if it's <= 0.0, // everything has faded away, and we restart if (alpha <= 0.0) Start(); }
The last piece of interesting code is the ValidatePixelFormat
function. Due to limitations in the SetPixelFormat
function, in order to implement this function we have to go through some hoops. First I create a dummy window and an OpenGL context for it. Then I can call ValidatePixelFormat
. Inside this function it can use OpenGL functions to query a device's capabilities. Then once that function returns, I destroy the dummy window and rendering context and create the real window and context. Painful, but it works.
Scrolling text looks badly aliased. I wanted to solve this problem, so I figured out how to turn on FSAA if a video card supports it. Here's a look at the code:
// Overridden to enable multisampling (FSAA) int CStarWarsController::ValidatePixelFormat(HDC hdc,int suggestedFormat) { HDC hDC = wglGetCurrentDC(); PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARB = (PFNWGLCHOOSEPIXELFORMATARBPROC) wglGetProcAddress("wglChoosePixelFormatARB"); if (!wglChoosePixelFormatARB) return suggestedFormat; if (!GLExtensionExists("WGL_ARB_multisample ")) return suggestedFormat; int pixelFormat; BOOL bStatus; UINT numFormats; float fAttributes[] = {0,0}; int iAttributes[] = { WGL_DRAW_TO_WINDOW_ARB,GL_TRUE, WGL_SUPPORT_OPENGL_ARB,GL_TRUE, WGL_ACCELERATION_ARB,WGL_FULL_ACCELERATION_ARB, WGL_COLOR_BITS_ARB,24, WGL_ALPHA_BITS_ARB,8, WGL_DEPTH_BITS_ARB,16, WGL_STENCIL_BITS_ARB,0, WGL_DOUBLE_BUFFER_ARB,GL_TRUE, WGL_SAMPLE_BUFFERS_ARB,GL_TRUE, WGL_SAMPLES_ARB,4, 0,0}; bStatus = wglChoosePixelFormatARB(hDC,iAttributes, fAttributes,1,&pixelFormat,&numFormats); if ((bStatus == GL_TRUE) && (numFormats == 1)) { m_bMultiSample = true; return pixelFormat; } // ok that failed, try using 2 samples now instead of 4 iAttributes[19] = 2; bStatus = wglChoosePixelFormatARB(hDC,iAttributes, fAttributes,1,&pixelFormat,&numFormats); if ((bStatus == GL_TRUE) && (numFormats == 1)) { m_bMultiSample = true; return pixelFormat; } // failed, return the suggested format and continue return suggestedFormat; }
Other uses
This example shows you how to create a standalone OpenGL application. However, you can easily use my control as a child window. I wrote a Euchre game, and I embedded the OpenGL control + StarWars controller into my About Box. It makes a nice effect.