My game looks awfull using stereoscopy with NVIDIA's Vision drivers

This article assumes you're a bit familiar with NVIDIA's 3D Vision technology, and you're having trouble with it.

Vision technology is great. Turn on a button in the Control Panel, calibrate the glasses, and you're ready to go. You're amazed by how all you're games are now experienced in stereoscopic 3D, except you now go run your own Ogre application... and it's all screwed!

Just like in this picture:

Your first suspicion is a driver bug. It's a very new technology so this would be no surprise. Even many AAA games don't actually work fine with it. You start playing with your code, and found out it has (most likely) something to do with hardware skeletal animations. You start seeing all NVIDIA's slides from GDC & SIGGRAPH about their vision drivers (since there's not too much documentation) and spot that the Vision driver patches your vertex shaders on the fly to produce left & right eye images.
Conclusion: the driver can't handle my skeletal animation code.

Guess again... you're wrong. It's a bug in your code. NVIDIA drivers are just fine.

Cause of the problem:

Your vertex shader probably looks something like this:

float4 BlendedPos = 0;
int i;
for( i=0; i < NUM_BONES_PER_VERTEX; ++i )
	BlendedPos += float4( mul(worldMatrix3x4Array[inBlendIdx[i]], inPosition).xyz, 1.0f ) * inBlendWght[i];

outPosition = mul( viewProjMatrix, BlendedPos );

This shader works fine without stereoscopy, and it's in fact the same code that ships with the Ogre samples, and probably the same code in many, many AAA games.
But when you turn on stereoscopy, it blows up. What's wrong?

What's wrong is that this code operates with the assumption that the sum of all weights are 1.0.
In other words:

inBlendWght[0] + inBlendWght[1] + ... + inBlendWght[n] = 1

When you're using more than one bone per vertex, usually Ogre does this for you in the function Mesh::_rationaliseBoneAssignments()
But chances are, sometimes (specially when using only one bone per vertex) this function didn't fix it for you.

Let's analyze what happens when inBlendWght[0] is 0.8, and NUM_BONE_PER_VERTEX is 1:

BlendedPos += float4( mul(worldMatrix3x4Array[inBlendIdx[i]], inPosition).xyz, 1.0f ) * 0.8f;

outPosition = mul( viewProjMatrix, BlendedPos );

Later, the GPU projects the vertices, between the vertex and pixel shader stages:

pixelPos = outPosition.xyz / outPosition.w;

Let's analyze it again, but putting the weights outside of the variable:

pixelPos = (outPosition.xyz * 0.8) / (outPosition.w * 0.8);

0.8 is both in the numerator and denominator, therefore they cancel each other, and is the same as if all vertices had inBlendWght[0] = 1.0!
The result, just out of luck, is rendered correctly.

If it's rendered correctly, then why it isn't in stereoscopy?

If you read the GDC papers from NVIDIA again, the vision drivers' magic lies in using the W component from your vertex. Althought xyz / w gives a reasonable correct result, the w value is still wrong, and hence you get serious artifacts. Vision driver assumes your W coordinate is correct.

Ok then, how do I fix it?

There are two ways to fix it:

  • The easy way: FIX YOUR MESH. Go back to Blender/Maya/3DS Max and tell your modeller to assign all the vertices a blend weight of 1.0, or make a simple tool that loads your .mesh files, modifies the blend weights, and saves the mesh again. Or do this at runtime. If you're using Blender 2.49b, you may want to check out use this script which performs it automatically.

  • The hard way: Modify your shader. In some cases, the artist put on purpose a weight different than 1. And this is critical for you. Therefore, you will need to take into account the vertex position using the world matrix alone (not the one passed from the world array), with the remainder of the weight.

The modified code should look like this:

float4 worldWeight = 0;
for( i=0; i < NUM_BONES_PER_VERTEX; ++i )
	worldWeight += inBlendWght[i];

worldWeight = 1.0f / worldWeight;

float4 BlendedPos = mul( worldMatrix, inPosition ) * worldWeight; //worldMatrix is using world_matrix material binding
int i;
for( i=0; i < NUM_BONES_PER_VERTEX; ++i )
	BlendedPos += float4( mul(worldMatrix3x4Array[inBlendIdx[i]], inPosition).xyz, 1.0f ) * inBlendWght[i];

Moral of the article

This is a single example of an application going mad when stereoscopy is turned on. When you get artifacts, always ensure you're computing the correct W component. Almost always this is the cause of the problem, since the whole technology actually boils down to this. There's not much more to it.

Useful links:

  2. NVIDIA GDC 2008 Paper