Stereoscopic - Cannot perceive depth difference

Hi everyone,

I am using OpenGL to build some basic psychological experiments, and many of these are done using stereoscopic displays. In this case, I am drawing 3D arrows on the screen at various different times, and having users respond to the direction of them.

I am having a problem with being unable to perceive differences in depth plane of objects in relationship to each other. The objects do appear ‘3D’ and ‘pop out’, but I cannot perceive depth differences. I am using an orthographic projection matrix (there seems to be a difference in depth on the perspective projection, but this is likely due solely the additional depth cue of the perspective…) I am primarily using GLUT.

There’s enough code where it isn’t practical to share all of the code right now, but below are a few of the areas I think may be related to the issue. However, if anyone wants to dive into the full set of code, I can share that as well. If any additional information would be helpful, or other snippets, please let me know.

Please feel free to ask anything that you think will help you understand what I am doing, and I’ll provide whatever I can to help.

(Disclaimer: I realize that much of my code is inefficient and not the neatest)

Here is the Display() function, with some omissions to simplify.


	xyz eyePosition;
	GLdouble left = 0.1;
	GLdouble right = 0.1;
	GLdouble Far = 100000;
	GLdouble ratio = camera.getRatio();
	GLdouble radians = DTOR * camera.getAperture() / 2;
	GLdouble Near = 0.0001;
	GLdouble wd2 = Near * tan(radians);
	GLdouble ndfl = Near / camera.getFocalLength();
	GLdouble top = wd2;
	GLdouble bottom = -wd2;

	glDrawBuffer(GL_BACK_LEFT);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glDrawBuffer(GL_BACK_RIGHT);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		CROSSPRODUCT(camera.viewDirection, camera.viewUpDirection, eyePosition);
		Normalize(&eyePosition);
		eyePosition.x *= camera.getEyeSeparation() / 1.5;
		eyePosition.y *= camera.getEyeSeparation() / 1.5;
		eyePosition.z *= camera.getEyeSeparation() / 1.5;

		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		left = -ratio * wd2 - 0.5 * camera.getEyeSeparation() * ndfl;
		right = ratio * wd2 - 0.5 * camera.getEyeSeparation() * ndfl;
		top = wd2;
		bottom = -wd2;

		if (experiment.condition == 0)     //Perspective
		{
			glFrustum(left, right, bottom, top, Near, Far);
		}
		else if (experiment.condition == 1)     //Orthographic
		{
			glOrtho(left, right, bottom, top, Near, Far);
		}

		glMatrixMode(GL_MODELVIEW);
		glDrawBuffer(GL_BACK_RIGHT);
		glLoadIdentity();
		gluLookAt(camera.viewPosition.x + eyePosition.x, camera.viewPosition.y + eyePosition.y, camera.viewPosition.z + eyePosition.z,
			camera.viewPosition.x + eyePosition.x + camera.viewDirection.x,
			camera.viewPosition.y + eyePosition.y + camera.viewDirection.y,
			camera.viewPosition.z + eyePosition.z + camera.viewDirection.z,
			camera.viewUpDirection.x, camera.viewUpDirection.y, camera.viewUpDirection.z);
		Lighting();
		DrawStimuli();     //This function is where I actually draw the arrows.  I'll add this separately below

		//This is all repeated for the other eye, in the positive direction

And here is the DrawStimuli() function


	GLfloat specular[4] = { 1.0, 1.0, 1.0, 1.0 };
	GLfloat shiny[1] = { 5.0 };

	glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, specular);
	glMaterialfv(GL_FRONT_AND_BACK, GL_SHININESS, shiny);

//I have a class called 'arrow' that contains the x,y,z coordinates for a cube (stem of the arrow) and four triangles (head of the arrow).  These values are all initialized here, but omitted from this post

//The experiment class contains most of the world coordinates, etc.
		experiment.xWidth = 0.1f/10000.0f;
		experiment.yHeight = 0.0f;
		experiment.trial.DistZDepth = 0.0f;
		experiment.trial.TargetZDepth = 0.0f;
		experiment.zInitialDepth = -6.0f;		//This is initial, starting depth
		experiment.planeDistance = 5.0f;

//There are 9 different cases here, depending on which depth plane for dist and target, but I'm only showing 2 for simplicity
switch (experiment.orderVector[experiment.trial.trialNumber] % 9)
		{
		case 0:
			experiment.trial.DistZDepth = -experiment.planeDistance + experiment.zInitialDepth;
			experiment.trial.distPlane = 1;
			experiment.trial.targetPlane = 1;
			experiment.trial.TargetZDepth = -experiment.planeDistance + experiment.zInitialDepth;
			break;
		case 1:
			experiment.trial.DistZDepth = -experiment.planeDistance + experiment.zInitialDepth;
			experiment.trial.distPlane = 1;
			experiment.trial.targetPlane = 2;
			experiment.trial.TargetZDepth = 0.1f + experiment.zInitialDepth;
			break;
		}

			glPushMatrix();
			glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);		//Clear screen and depth buffers

			//Build distractor arrows
			if (experiment.trial.distDirection == NEUTRAL)
			{
				//Build d1 arrow (far left)
				glLoadIdentity();										//Reset the view to the center of the screen
				glTranslatef(-(experiment.xWidth*0.5f), experiment.yHeight, experiment.trial.DistZDepth);
				buildArrow(neutralDistractor);
				//Build d2 arrow (middle left)
				glLoadIdentity();										//Reset the view to the center of the screen
				glTranslatef(-(experiment.xWidth*0.25f), experiment.yHeight, experiment.trial.DistZDepth);
				buildArrow(neutralDistractor);
				//Build d3 arrow (middle right)
				glLoadIdentity();										//Reset the view to the center of the screen
				glTranslatef((experiment.xWidth*0.25f), experiment.yHeight, experiment.trial.DistZDepth);
				buildArrow(neutralDistractor);
				//Build d4 arrow (far right)
				glLoadIdentity();										//Reset the view to the center of the screen
				glTranslatef((experiment.xWidth*0.5f), experiment.yHeight, experiment.trial.DistZDepth);
				buildArrow(neutralDistractor);
			}
//There are two other directions (left and right) that repeat this code but build different arrows in the buildArrow(arrow) function.  Omitted for simplicity.
			//Update type to Target
			rightArrow.type = TARGET;
			leftArrow.type = TARGET;

			//Build target arrow
			if (experiment.trial.targetDirection == LEFT)
			{
				glLoadIdentity();										//Reset the view to the center of the screen
				glTranslatef(0.0f, experiment.yHeight, experiment.trial.TargetZDepth);
				buildArrow(leftArrow);
			}
			else if (experiment.trial.targetDirection == RIGHT)
			{
				glLoadIdentity();										//Reset the view to the center of the screen
				glTranslatef(0.0f, experiment.yHeight, experiment.trial.TargetZDepth);
				buildArrow(rightArrow);
			}

			glPopMatrix();

Finally, here is the buildArrow function. It’s messy and possibly unclear without seeing the entire arrow class… but here it is!


GLvoid buildArrow(Arrow a)
{
	xyz n;

	n = CalculateNormal(a.t1.p1, a.t1.p2, a.t1.p3);
	glBegin(GL_TRIANGLES);		//Base
	glNormal3f(n.x, n.y, n.z);
	glVertex3f(a.t1.p1.x, a.t1.p1.y, a.t1.p1.z);
	glVertex3f(a.t1.p2.x, a.t1.p2.y, a.t1.p2.z);
	glVertex3f(a.t1.p3.x, a.t1.p3.y, a.t1.p3.z);
	glEnd();
	n = CalculateNormal(a.t2.p1, a.t2.p2, a.t2.p3);
	glBegin(GL_TRIANGLES);		//Top
	glNormal3f(n.x, n.y, n.z);
	glVertex3f(a.t2.p1.x, a.t2.p1.y, a.t2.p1.z);
	glVertex3f(a.t2.p2.x, a.t2.p2.y, a.t2.p2.z);
	glVertex3f(a.t2.p3.x, a.t2.p3.y, a.t2.p3.z);
	glEnd();
	n = CalculateNormal(a.t3.p1, a.t3.p2, a.t3.p3);
	glBegin(GL_TRIANGLES);		//Bottom
	glNormal3f(n.x, n.y, n.z);
	glVertex3f(a.t3.p1.x, a.t3.p1.y, a.t3.p1.z);
	glVertex3f(a.t3.p2.x, a.t3.p2.y, a.t3.p2.z);
	glVertex3f(a.t3.p3.x, a.t3.p3.y, a.t3.p3.z);
	glEnd();
	n = CalculateNormal(a.t4.p1, a.t4.p2, a.t4.p3);
	glBegin(GL_TRIANGLES);		//Back
	glNormal3f(n.x, n.y, n.z);
	glVertex3f(a.t4.p1.x, a.t4.p1.y, a.t4.p1.z);
	glVertex3f(a.t4.p2.x, a.t4.p2.y, a.t4.p2.z);
	glVertex3f(a.t4.p3.x, a.t4.p3.y, a.t4.p3.z);
	glEnd();

	//In this iteration, we are only drawing the quads if it is a distractor and the type is neutral
	if (a.type == DISTRACTOR && experiment.trial.distDirection == NEUTRAL)
	{
		//glColor3f(0.2f, 0.2f, 0.2f);
		n = CalculateNormal(a.s1.p1, a.s1.p2, a.s1.p3);
		glBegin(GL_QUADS);		//Top
		glNormal3f(n.x, n.y, n.z);
		glVertex3f(a.s1.p1.x, a.s1.p1.y, a.s1.p1.z);
		glVertex3f(a.s1.p2.x, a.s1.p2.y, a.s1.p2.z);
		glVertex3f(a.s1.p3.x, a.s1.p3.y, a.s1.p3.z);
		glVertex3f(a.s1.p4.x, a.s1.p4.y, a.s1.p4.z);
		glEnd();
		n = CalculateNormal(a.s2.p1, a.s2.p2, a.s2.p3);
		glBegin(GL_QUADS);		//Back
		glNormal3f(n.x, n.y, n.z);
		//glColor3f(0.0f, 0.0f, 0.0f);
		glVertex3f(a.s2.p1.x, a.s2.p1.y, a.s2.p1.z);
		glVertex3f(a.s2.p2.x, a.s2.p2.y, a.s2.p2.z);
		glVertex3f(a.s2.p3.x, a.s2.p3.y, a.s2.p3.z);
		glVertex3f(a.s2.p4.x, a.s2.p4.y, a.s2.p4.z);
		glEnd();
		n = CalculateNormal(a.s3.p1, a.s3.p2, a.s3.p3);
		glBegin(GL_QUADS);		//Bottom
		glNormal3f(n.x, n.y, n.z);
		glVertex3f(a.s3.p1.x, a.s3.p1.y, a.s3.p1.z);
		glVertex3f(a.s3.p2.x, a.s3.p2.y, a.s3.p2.z);
		glVertex3f(a.s3.p3.x, a.s3.p3.y, a.s3.p3.z);
		glVertex3f(a.s3.p4.x, a.s3.p4.y, a.s3.p4.z);
		glEnd();
		n = CalculateNormal(a.s4.p1, a.s4.p2, a.s4.p3);
		glBegin(GL_QUADS);		//Front
		glNormal3f(n.x, n.y, n.z);
		glVertex3f(a.s4.p1.x, a.s4.p1.y, a.s4.p1.z);
		glVertex3f(a.s4.p2.x, a.s4.p2.y, a.s4.p2.z);
		glVertex3f(a.s4.p3.x, a.s4.p3.y, a.s4.p3.z);
		glVertex3f(a.s4.p4.x, a.s4.p4.y, a.s4.p4.z);
		glEnd();
		n = CalculateNormal(a.s5.p1, a.s5.p2, a.s5.p3);
		glBegin(GL_QUADS);		//Left
		glNormal3f(n.x, n.y, n.z);
		glVertex3f(a.s5.p1.x, a.s5.p1.y, a.s5.p1.z);
		glVertex3f(a.s5.p2.x, a.s5.p2.y, a.s5.p2.z);
		glVertex3f(a.s5.p3.x, a.s5.p3.y, a.s5.p3.z);
		glVertex3f(a.s5.p4.x, a.s5.p4.y, a.s5.p4.z);
		glEnd();
		n = CalculateNormal(a.s6.p1, a.s6.p2, a.s6.p3);
		glBegin(GL_QUADS);		//Right
		glNormal3f(n.x, n.y, n.z);
		glVertex3f(a.s6.p1.x, a.s6.p1.y, a.s6.p1.z);
		glVertex3f(a.s6.p2.x, a.s6.p2.y, a.s6.p2.z);
		glVertex3f(a.s6.p3.x, a.s6.p3.y, a.s6.p3.z);
		glVertex3f(a.s6.p4.x, a.s6.p4.y, a.s6.p4.z);
		glEnd();
	}
}

EDIT: Forgot to include some important stuff.
Video Card: NVidia Quadro K600
Monitor: Acer GD235HZ
OS: Windows 7
3D Glasses: Nvidia 3D Vision

I am using an orthographic projection matrix

Your code calls glFrustum to set up the projection matrix, that won’t give you an orthographic projection? Usually one shifts the eye points along the camera x axis to the left and right and constructs the view frusta such that there is a plane (parallel to the camera xy plane) where they coincide - in particular they are not symmetric frusta, but the left one is slightly skewed to the right and the other way round for the right one.

Other than that, since you are doing a psychological experiment I guess you already know that some aspects of human vision can not be explained entirely by optical geometry. If you present two abstract objects (where the viewer has no knowledge about their absolute or relative sizes) without a surrounding environment it becomes very hard to distinguish two same-size objects at different depths from two different-size objects at the same depth.

There’s actually an if statement that uses glFrustum for perspective and glOrtho for orthographic… that’s part of what I omitted from the post. I’ll edit the code to show both, for those who go look at it in the future. Nice catch here.

Yeah, this is a limitation where I was hoping that exaggerated differences on the depth plane would make this at least subtly different. In the real world, you can perceive two different objects at different differences as the same size (assuming that the larger one is behind, of course) and still tell the difference in depth, albeit with more uncertainty. I’m hoping to find a way around this. While I imagine that you may well be right, my goal is to find a way to give this perception without including additional cues.