Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Win32 OpenGL Framework - Star Wars scrolling Text

0.00/5 (No votes)
6 Aug 2002 1  
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.

Sample Image - starwars.gif

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 -

  1. MFC and
  2. 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 CObjects

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here