Win32 OpenGL 框架 - 星球大战滚动文本






4.38/5 (6投票s)
2002年8月7日
4分钟阅读

149469

5580
一篇文章详细介绍了一个包含星球大战风格滚动文本的项目。该项目完全使用 OpenGL 完成,并展示了一些高级 OpenGL 主题。此外,还提供了一个用于在 Win32 程序中使用 OpenGL 窗口的框架。
动机
我看到了 Pablo van der Meer 关于他的 CStarWarsCtrl 的文章。我觉得它非常有趣,但我不喜欢它使用了 -
- MFC 和
StretchBlt
.
因此,我开始着手用 OpenGL 来重新实现它,同时使其对 Win32 友好。
这是什么?
本文提供了一个 Win32 OpenGL 框架。它通过隐藏大部分 OpenGL 初始化/关闭代码,为您提供了便利。文章展示了如何使用该框架创建类似星球大战的滚动文本效果。
Win32 OpenGL
最困难的部分是在 Win32 下设置 OpenGL。首先,您需要使用 CS_OWNDC
样式创建一个窗口类,并查询像素格式。一旦有了这些,您就可以创建一个 OpenGL 渲染上下文并将其附加到窗口的 DC 上。经过一些文档查阅后,我发现这其实并不难。我经常编写许多功能强大的 Win32 控件。所以在这里我坚持了我的计划。我制作了一个执行所有 OpenGL 工作的 Win32 控件。您只需要为控件提供一个 COpenGLWndController
类和一个请求的像素格式,控件就会利用它们。我们来看看它。
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 };
创建
要创建一个 OpenGL 窗口,请使用此函数
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);
人们只需创建一个 COpenGLWndController
的子类,并实现 WindowSized
、vDraw
、Init
和 Close
。WindowSized
会响应 WM_SIZE
消息被调用,您可以在这里更改您的 OpenGL 视口。vDraw
是渲染场景的函数。不要将其与 Draw
混淆。Draw
是您调用的公共函数,用于重绘窗口 - 它会处理诸如交换缓冲区之类的后台操作。Draw
最终也会调用 vDraw
。Init
在 OpenGL 窗口创建其渲染上下文并准备就绪后被调用。现在您可以根据需要加载纹理或初始化 OpenGL。Close
类似,您可以在这里删除任何 OpenGL 纹理/对象等。ValidatePixelFormat
不需要被重写,但可以被重写。您可以使用此函数来调整像素格式,如果需要,可以返回一个新的像素格式。在我的实现中,我使用它来开启 FSAA(全屏抗锯齿)。
实现
让我们看看我们的子类是如何工作的 - CStarWarsController
。WindowSized
的代码非常容易理解。
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); }
Init
的代码初始化使用的字体。
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); }
Close
的代码清理使用的字体并删除我们的 CObject
对象。
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; } } }
我提到了 CObject
类。我在控制器中使用这个类,因为它代表屏幕上移动的对象。每一行文本都被视为一个对象。每个对象都有一个起始点、一个移动向量和一个当前位置。因此,对于任何时间 t
,我都可以从起始点和移动向量计算出当前位置。CObject
有一个可重写的函数 Draw()
。我提供了 CObject
的两个子类:CTextObject
和 CTexturedQuad
。移动的标志是一个 CTexturedQuad
。
时间偏移可能需要解释一下。对象存储在一个数组中。第一个对象需要跟在其他对象后面才能看起来好看。每个对象都有相同的起始点。在这个例子中,它是 0,-4,0。但是每个对象都有一个出现的时间偏移。在时间 0 时,它们将出现在 0,-4,0。在时间偏移 2 时,它们会更靠近观察者,因为它们落后了 2 秒。因此,数组中的所有对象都有递增的时间偏移。文本对象通常需要 2 秒的时间偏移。这就是对象的间隔方式。这意味着您可以通过更改时间偏移字段来随意设置它们的间隔。
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; };
在我的示例中,它会持续渲染。CStarWarsController
有一个名为 Idle
的函数,它会移动屏幕上的所有对象和星星。代码很简单,就是简单的向量数学。
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; } } }
同样,vDraw
函数除了渲染星星和调用 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(); }
最后一段有趣的代码是 ValidatePixelFormat
函数。由于 SetPixelFormat
函数的限制,为了实现这个函数,我们必须经过一些曲折。首先,我创建一个虚拟窗口和它的 OpenGL 上下文。然后我就可以调用 ValidatePixelFormat
。在这个函数内部,它可以使用 OpenGL 函数查询设备的性能。一旦该函数返回,我就会销毁虚拟窗口和渲染上下文,并创建实际的窗口和上下文。很麻烦,但有效。
滚动的文本看起来锯齿严重。我想解决这个问题,所以我弄清楚了如何在视频卡支持的情况下开启 FSAA。这里是代码
// 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; }
其他用途
本示例展示了如何创建独立的 OpenGL 应用程序。但是,您也可以轻松地将我的控件用作子窗口。我写了一个 Euchre 游戏,并将 OpenGL 控件 + StarWars 控件嵌入到我的“关于”框中。这效果很不错。