stereo geometry in OpenGL

Dislocated Lunate

OpenGL is a powerful cross-platform graphics API which has many benefits - specifically a wide range of hardware and software support and support for quad-buffered stereo rendering.  Quad-buffering is the ability to render into left and right front and back buffers independently.  The front left and front right buffers displaying the stereo images can be swapped in sync with shutterglasses while the back left and back right buffers are being updated - giving a smooth stereoscopic display.  It is also relatively easy to learn with a bit of application  - the code below was written inside of 3 months from the time I started programming due in no small part to the many resources available on the web.

When rendering in OpenGL, understanding the geometry behind what you want to achieve is essential.  Toed-in stereo is quick and easy but does have the side effect that a bit of keystone distortion creeps into both left and right views due to the difference between the rendering plane and the viewing plane.  This is not too much of a problem in central portions of a scene, but becomes significant at the screen edges.  Asymmetric frustum parallel projection (equivalent to lens-shift in photography) corrects for this keystone distortion and puts the rendering plane and viewing plane in the same orientation.  It is essential that when using the asymmetric frustum technique that the rendering geometry closely matches the geometry of the viewing system.  Failure to match rendering and viewing geometry results in a distorted image delivered to both eyes and can be more disturbing than the distortion from toed-in stereo.

Paul Bourke has an excellent site with examples of stereoscopic rendering in OpenGL.  If you are interested in creating stereo views in OpenGL, it is worth spending time working out the geometry of toed-in (which is quite easy but introduces distortion into the viewing system) and asymmetric frustum parallel axis projection for yourself.  Below is my method - familiarity with OpenGL, GLUT and C is assumed and you need to have a graphics card which is capable of quad-buffering:

---advertisement---


Toed-in stereo


Toed-in geometry

The idea is to use gluLookAt to set the camera position and point it at the middle of the screen from the two eye positions:

//toed-in stereo

float depthZ = -10.0;                                      //depth of the object drawing

double fovy = 45;                                          //field of view in y-axis
double aspect = double(screenwidth)/double(screenheight);  //screen aspect ratio
double nearZ = 3.0;                                        //near clipping plane
double farZ = 30.0;                                        //far clipping plane
double screenZ = 10.0;                                     //screen projection plane
double IOD = 0.5;                                          //intraocular distance

void init(void)
{
  glViewport (0, 0, screenwidth, screenheight);            //sets drawing viewport
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(fovy, aspect, nearZ, farZ);               //sets frustum using gluPerspective
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
}
GLvoid display(GLvoid)
{
  glDrawBuffer(GL_BACK);                                   //draw into both back buffers
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);      //clear color and depth buffers

  glDrawBuffer(GL_BACK_LEFT);                              //draw into back left buffer
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();                                        //reset modelview matrix
  gluLookAt(-IOD/2,                                        //set camera position  x=-IOD/2
            0.0,                                           //                     y=0.0
            0.0,                                           //                     z=0.0
            0.0,                                           //set camera "look at" x=0.0
            0.0,                                           //                     y=0.0
            screenZ,                                       //                     z=screenplane
            0.0,                                           //set camera up vector x=0.0
            1.0,                                           //                     y=1.0
            0.0);                                          //                     z=0.0
 

  glPushMatrix();
  {
    glTranslatef(0.0, 0.0, depthZ);                        //translate to screenplane
    drawscene();
  }
  glPopMatrix();


  glDrawBuffer(GL_BACK_RIGHT);                             //draw into back right buffer
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();                                        //reset modelview matrix
  gluLookAt(IOD/2, 0.0, 0.0, 0.0, 0.0, screenZ,            //as for left buffer with camera position at:
            0.0, 1.0, 0.0);                                //                     (IOD/2, 0.0, 0.0)

  glPushMatrix();
  {
    glTranslatef(0.0, 0.0, depthZ);                        //translate to screenplane
    drawscene();
  }
  glPopMatrix();

 
  glutSwapBuffers();
}

---advertisement---


Asymmetric frustum parallel axis projection stereo


This is a bit more complex, as one needs to set up an asymmetric frustum first before moving the camera viewpoint.  In OpenGL, the asymmetric frustum is set up with the camera at the (0.0, 0.0, 0.0) position and then needs to be translated by IOD/2 to make sure that there is no parallax difference at the screen plane depth.  Geometry for the right viewing frustum is depicted below:

Asymmetric frustum geometry

To set up an asymmetric frustum, the main thing is deciding how much to shift the frustum by.  This is quite easy as long as we assume that we only want to move the camera by +/- IOD/2 along the X-axis.  From the geometry it is evident that the ratio of frustum shift to the near clipping plane is equal to the ratio of IOD/2 to the distance from the screenplane.

I decided to use a function call to set up the frustum on initiallization and anytime the viewport is changed:

#define DTR 0.0174532925

struct camera
{
    GLdouble leftfrustum;
    GLdouble rightfrustum;
    GLdouble bottomfrustum;
    GLdouble topfrustum;
    GLfloat modeltranslation;
} leftCam, rightCam;

float depthZ = -10.0;                                      //depth of the object drawing

double fovy = 45;                                          //field of view in y-axis
double aspect = double(screenwidth)/double(screenheight);  //screen aspect ratio
double nearZ = 3.0;                                        //near clipping plane
double farZ = 30.0;                                        //far clipping plane
double screenZ = 10.0;                                     //screen projection plane
double IOD = 0.5;                                          //intraocular distance

void setFrustum(void)
{
    double top = nearZ*tan(DTR*fovy/2);                    //sets top of frustum based on fovy and near clipping plane
    double right = aspect*top;                             //sets right of frustum based on aspect ratio
    double frustumshift = (IOD/2)*nearZ/screenZ;

    leftCam.topfrustum = top;
    leftCam.bottomfrustum = -top;
    leftCam.leftfrustum = -right + frustumshift;
    leftCam.rightfrustum = right + frustumshift;
    leftCam.modeltranslation = IOD/2;

    rightCam.topfrustum = top;
    rightCam.bottomfrustum = -top;
    rightCam.leftfrustum = -right - frustumshift;
    rightCam.rightfrustum = right - frustumshift;
    rightCam.modeltranslation = -IOD/2;
}
void init(void)
{
  glViewport (0, 0, screenwidth, screenheight);            //sets drawing viewport
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
}
GLvoid reshape(int w, int h)
{
    if (h==0)
    {
        h=1;                                               //prevent divide by 0
    }
    aspect=double(w)/double(h);
    glViewport(0, 0, w, h);
    setFrustum();
}
GLvoid display(GLvoid)
{
  glDrawBuffer(GL_BACK);                                   //draw into both back buffers
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);      //clear color and depth buffers

  glDrawBuffer(GL_BACK_LEFT);                              //draw into back left buffer
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();                                        //reset projection matrix
  glFrustum(leftCam.leftfrustum, leftCam.rightfrustum,     //set left view frustum
            leftCam.bottomfrustum, leftCam.topfrustum,
            nearZ, farZ);
  glTranslatef(leftCam.modeltranslation, 0.0, 0.0);
       //translate to cancel parallax
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity);
  glPushMatrix();
  {
    glTranslatef(0.0, 0.0, depthZ);                        //translate to screenplane
    drawscene();
  }
  glPopMatrix();


  glDrawBuffer(GL_BACK_RIGHT);                             //draw into back right buffer
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();                                        //reset projection matrix
  glFrustum(rightCam.leftfrustum, rightCam.rightfrustum,   //set left view frustum
            rightCam.bottomfrustum, rightCam.topfrustum,
            nearZ, farZ);
  glTranslatef(rightCam.modeltranslation, 0.0, 0.0);
      //translate to cancel parallax
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity);


  glPushMatrix();
  {
    glTranslatef(0.0, 0.0, depthZ);                        //translate to screenplane
    drawscene();
  }
  glPopMatrix();

 
  glutSwapBuffers();
}

home

---advertisement---