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:
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;
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;
virtual void Close() = 0;
};
Creation
To create an OpenGL window, use this function:
BOOL RegisterOpenGLWindow(HINSTANCE hInst);
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);
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;
QueryPerformanceCounter(&now);
m_fTimeElapsed = ((float)(now.QuadPart - m_start.QuadPart)
/(float)m_freq.QuadPart);
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);
}
m_fTimeElapsed = ((float)(now.QuadPart -
m_starStart.QuadPart)/(float)m_freq.QuadPart);
for (int i=0;i<m_iNumStars;++i)
{
m_pStars[i].m_curPos[2] = m_pStars[i].m_start.z +
m_pStars[i].speed.z *
(m_fTimeElapsed - m_pStars[i].timeOffset);
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
.
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);
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
{
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();
}
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;
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);
alpha = 3.75f - sqrtf(distance);
if (alpha > 1.0f)
alpha = 1.0f;
else if (alpha < 0.0)
alpha = 0.0;
glPushMatrix();
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();
}
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:
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;
}
iAttributes[19] = 2;
bStatus = wglChoosePixelFormatARB(hDC,iAttributes,
fAttributes,1,&pixelFormat,&numFormats);
if ((bStatus == GL_TRUE) && (numFormats == 1))
{
m_bMultiSample = true;
return pixelFormat;
}
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.