stereo
geometry in OpenGL
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
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:
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---