65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.38/5 (6投票s)

2002年8月7日

4分钟阅读

viewsIcon

149469

downloadIcon

5580

一篇文章详细介绍了一个包含星球大战风格滚动文本的项目。该项目完全使用 OpenGL 完成,并展示了一些高级 OpenGL 主题。此外,还提供了一个用于在 Win32 程序中使用 OpenGL 窗口的框架。

Sample Image - starwars.gif

动机

我看到了 Pablo van der Meer 关于他的 CStarWarsCtrl 的文章。我觉得它非常有趣,但我不喜欢它使用了 -

  1. MFC 和
  2. 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 的子类,并实现 WindowSizedvDrawInitCloseWindowSized 会响应 WM_SIZE 消息被调用,您可以在这里更改您的 OpenGL 视口。vDraw 是渲染场景的函数。不要将其与 Draw 混淆。Draw 是您调用的公共函数,用于重绘窗口 - 它会处理诸如交换缓冲区之类的后台操作。Draw 最终也会调用 vDrawInit 在 OpenGL 窗口创建其渲染上下文并准备就绪后被调用。现在您可以根据需要加载纹理或初始化 OpenGL。Close 类似,您可以在这里删除任何 OpenGL 纹理/对象等。ValidatePixelFormat 不需要被重写,但可以被重写。您可以使用此函数来调整像素格式,如果需要,可以返回一个新的像素格式。在我的实现中,我使用它来开启 FSAA(全屏抗锯齿)。

实现

让我们看看我们的子类是如何工作的 - CStarWarsControllerWindowSized 的代码非常容易理解。

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 的两个子类:CTextObjectCTexturedQuad。移动的标志是一个 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 控件嵌入到我的“关于”框中。这效果很不错。

© . All rights reserved.