Thursday 29 November 2012

Using NGL with SDL

SDL is a very good library for games development and very useful for cross platform development. In this post I will explain how to install and configure SDL 2.0 (HG) for use with OpenGL and my NGL:: library. The source code can be downloaded using bzr branch http://nccastaff.bournemouth.ac.uk/jmacey/Code/SDLNGL from here

SDL installation

The latest version of SDL handles creating "core profile" OpenGL contexts on mac so this will be required. Earlier version of SDL will not work as they do not support the creation of the correct context for OpenGL under the mac. I decided to do a local install of SDL and if you wish to use this in the Labs at the University you will have to do the same thing as you don't have root permission to install the libs. The process of installation is similar to the one outlined here and I'm going to install the libraries in a directory called $(HOME)/SDL2.0 this is important as the makefile will also use this location to find the sdl2-config script at a later date.

The following commands will download and install the libraries and build it into the correct directory.
mkdir SDL2.0
tar vfxz SDL-2.0.tar.gz 
cd SDL-2.0.0-6673/
./configure --prefix=/home/jmacey/SDL2.0 (change to your home dir)
make -j 8
make install
This will install everything into the SDL2 directory and you will have a structure like this
bin include lib share
To test this is working do the following
cd ~/SDL2.0/bin
./sdl2-config --cflags --libs
-I/Volumes/home/jmacey/SDL2.0/include/SDL2 -D_THREAD_SAFE
-L/Volumes/home/jmacey/SDL2.0/lib -lSDL2

SDL NGL Demo

The demo is split into two main modules. The main.cpp file will create the SDL and OpenGL context, and handle the processing of events. The NGLDraw class will contain all OpenGL setup and drawing routines.

Setup and basic SDL

To use SDL we need to include the <SDL.h> header, this will be placed in the path by the following command in the Qt .pro file.
QMAKE_CXXFLAGS+=$$system($$(HOME)/SDL2.0/bin/sdl2-config  --cflags)
message(output from sdl2-config --cflags added to CXXFLAGS= $$QMAKE_CXXFLAGS)

LIBS+=$$system($$(HOME)/SDL2.0/bin/sdl2-config  --libs)
message(output from sdl2-config --libs added to LIB=$$LIBS)
For more info see this post

First we need to initialise the SDL video subsystem using the following command

// Initialize SDL's Video subsystem
if (SDL_Init(SDL_INIT_VIDEO) < 0 )
{
  // Or die on error
  SDLErrorExit("Unable to initialize SDL");
}
There is also a helper function to exit SDL gracefully
void SDLErrorExit(const std::string &_msg)
{
  std::cerr<<_msg<<"\n";
  std::cerr<<SDL_GetError()<<"\n";
  SDL_Quit();
  exit(EXIT_FAILURE);
}
Next we create the basic window, in this case I get the size of the screen and configure the screen to be centred and half max screen width and height
// now get the size of the display and create a window we need to init the video
SDL_Rect rect;
SDL_GetDisplayBounds(0,&rect);
// now create our window
SDL_Window *window=SDL_CreateWindow("SDLNGL",SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,
                         rect.w/2,rect.h/2,
                         SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
// check to see if that worked or exit
if (!window)
{
 SDLErrorExit("Unable to create window"); 
}

Creating an OpenGL context

SDL 2.0 uses a SDL_GLContext to hold the information about the current GL context. There are many flags we need to setup our context and these are handled using the SDL_GL_SetAttribute function. I've also discovered on my linux build that some of these flags don't work and cause crashes (particularly creating a core profile context). To overcome this conditional compilation is used as shown in the following function.

SDL_GLContext createOpenGLContext(SDL_Window *window)
{
  // Request an opengl 3.2 context first we setup our attributes, if you need any
  // more just add them here before the call to create the context
  // SDL doesn't have the ability to choose which profile at this time of writing,
  // but it should default to the core profile
  // for some reason we need this for mac but linux crashes on the latest nvidia drivers
  // under centos
  #ifdef DARWIN
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
    SDL_GL_SetAttribute(SDL_GL_ACCELERATED_VISUAL, 1);
  #endif
  // set multi sampling else we get really bad graphics that alias
  SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
  SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES,4);
  // Turn on double buffering with a 24bit Z buffer.
  // You may need to change this to 16 or 32 for your system
  // on mac up to 32 will work but under linux centos build only 16
  SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
  // enable double buffering (should be on by default)
  SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
  //
  return SDL_GL_CreateContext(window);

}
Care must be taken with setting the depth size, under mac osx it works with 32 bit, under linux I set to 16 and on some machines 24 will work. The following code configures the GL context and clears the screen.
SDL_GLContext glContext=createOpenGLContext(window);
if(!glContext)
{
 SDLErrorExit("Problem creating OpenGL context");
}
// make this our current GL context (we can have more than one window but in this case not)
SDL_GL_MakeCurrent(window, glContext);
/* This makes our buffer swap syncronized with the monitor's vertical refresh */
SDL_GL_SetSwapInterval(1);
// now clear the screen and swap whilst NGL inits (which may take time)
glClear(GL_COLOR_BUFFER_BIT);
SDL_GL_SwapWindow(window);
Now this has been done we can use NGL and create our graphics. In this case the NGLDraw class is a re-working of the SimpleNGL demo, it initialises GLEW if required. The following code shows the creation of the NGLDraw class and the key and mouse processing.
NGLDraw ngl;
// resize the ngl to set the screen size and camera stuff
ngl.resize(rect.w,rect.h);
while(!quit)
{

 while ( SDL_PollEvent(&event) )
 {
  switch (event.type)
  {
   // this is the window x being clicked.
   case SDL_QUIT : quit = true; break;
   // process the mouse data by passing it to ngl class
   case SDL_MOUSEMOTION : ngl.mouseMoveEvent(event.motion); break;
   case SDL_MOUSEBUTTONDOWN : ngl.mousePressEvent(event.button); break;
   case SDL_MOUSEBUTTONUP : ngl.mouseReleaseEvent(event.button); break;
   case SDL_MOUSEWHEEL : ngl.wheelEvent(event.wheel);
   // if the window is re-sized pass it to the ngl class to change gl viewport
   // note this is slow as the context is re-create by SDL each time
   case SDL_WINDOWEVENT :
    int w,h;
    // get the new window size
    SDL_GetWindowSize(window,&w,&h);
    ngl.resize(w,h);
   break;

   // now we look for a keydown event
   case SDL_KEYDOWN:
   {
    switch( event.key.keysym.sym )
    {
     // if it's the escape key quit
     case SDLK_ESCAPE :  quit = true; break;
     case SDLK_w : glPolygonMode(GL_FRONT_AND_BACK,GL_LINE); break;
     case SDLK_s : glPolygonMode(GL_FRONT_AND_BACK,GL_FILL); break;
     case SDLK_f :
     SDL_SetWindowFullscreen(window,SDL_TRUE);
     glViewport(0,0,rect.w,rect.h);
     break;

     case SDLK_g : SDL_SetWindowFullscreen(window,SDL_FALSE); break;
     default : break;
    } // end of key process
   } // end of keydown

   default : break;
  } // end of event switch
 } // end of poll events

 // now we draw ngl
 ngl.draw();
 // swap the buffers
 SDL_GL_SwapWindow(window);

}
The most important call here is the SDL_GL_SwapWindow call which tells SDL to swap the buffers and re-draw.

NGLDraw class 

Most of the NGLDraw class is basic ngl code,  the constructor is used to initialise ngl and create the camera, light and materials.  The draw method grabs and instance of the primitives class and draws the teapot, both of which are similar to the Qt NGL demos. The main difference is the processing of the mouse input. I still use the same flags and attributes to store the rotations and position data, however the SDL mouse data is used to grab x,y and button values. This is shown in the following code.
void NGLDraw::mouseMoveEvent (const SDL_MouseMotionEvent &_event)
{
  if(m_rotate && _event.state &SDL_BUTTON_LMASK)
  {
    int diffx=_event.x-m_origX;
    int diffy=_event.y-m_origY;
    m_spinXFace += (float) 0.5f * diffy;
    m_spinYFace += (float) 0.5f * diffx;
    m_origX = _event.x;
    m_origY = _event.y;
    this->draw();

  }
  // right mouse translate code
  else if(m_translate && _event.state &SDL_BUTTON_RMASK)
  {
    int diffX = (int)(_event.x - m_origXPos);
    int diffY = (int)(_event.y - m_origYPos);
    m_origXPos=_event.x;
    m_origYPos=_event.y;
    m_modelPos.m_x += INCREMENT * diffX;
    m_modelPos.m_y -= INCREMENT * diffY;
    this->draw();
  }
}


void NGLDraw::mousePressEvent (const SDL_MouseButtonEvent &_event)
{
  // this method is called when the mouse button is pressed in this case we
  // store the value where the maouse was clicked (x,y) and set the Rotate flag to true
  if(_event.button == SDL_BUTTON_LEFT)
  {
    m_origX = _event.x;
    m_origY = _event.y;
    m_rotate =true;
  }
  // right mouse translate mode
  else if(_event.button == SDL_BUTTON_RIGHT)
  {
    m_origXPos = _event.x;
    m_origYPos = _event.y;
    m_translate=true;
  }
}

void NGLDraw::mouseReleaseEvent (const SDL_MouseButtonEvent &_event)
{
  // this event is called when the mouse button is released
  // we then set Rotate to false
  if (_event.button == SDL_BUTTON_LEFT)
  {
    m_rotate=false;
  }
  // right mouse translate mode
  if (_event.button == SDL_BUTTON_RIGHT)
  {
    m_translate=false;
  }
}

void NGLDraw::wheelEvent(const SDL_MouseWheelEvent &_event)
{

  // check the diff of the wheel position (0 means no change)
  if(_event.y > 0)
  {
    m_modelPos.m_z+=ZOOM;
    this->draw();
  }
  else if(_event.y <0 )
  {
    m_modelPos.m_z-=ZOOM;
    this->draw();
  }

  // check the diff of the wheel position (0 means no change)
  if(_event.x > 0)
  {
    m_modelPos.m_x-=ZOOM;
    this->draw();
  }
  else if(_event.x <0 )
  {
    m_modelPos.m_x+=ZOOM;
    this->draw();
  }
}
The rest of the code is fairly self explanatory if you've use NGL before.

No comments:

Post a Comment