Posted on 04-09-2008 under 3ds max, Algorithm

Ok, now it is time to actually show how the method I use works. This is C style code, but with some changes from my original code, for clarity. That is because my code handles the many layers of the 3ds max G-buffer and how pixels are weighted and works with transparency. Also, my code also has a method for simulating ambient radiance (more on that in a later post).

Read on for the actual code.

Input:

x
Screen space x-coordinate (unit: pixels).
y
Screen space y-coordinate (unit: pixels).
zbuffer
Depth buffer with pixel z coordinates in camera space.
normalbuffer
Buffer with pixel normals in camera space.
samples
Constant with number of samples
radius
Radius of sample sphere. Very scene scale dependant.
coneAngle
The cosine of the cone angle of the occlusion.
coneAngle = cos( angle / 2 * DEG_TO_RAD )
where angle around 150 is a good value.
strength
Multiplier that can be used to tweak the strength of the occlusion.

The code:

for each pixel(x, y)
{
	// Get current pixel depth
	float z = zbuffer[ x, y ];

	// Do nothing with pixels that are the background
	if( z <= -1.0e30f || z == 0.0f )
		continue;

	// Set initial variables
	float occlusion = 0.0f;
	int actualSamples = 0;

	// Map screen pixel to camera space point
	Ray cameraRay = MapScreenToCamRay( x, y );
	cameraRay.dir = Normalize( cameraRay.dir );
	Point3 p = -z * cameraRay.dir;
	Point3 normal = normalbuffer[ x, y ];

	for( int i = 0; i < samples; i++ )
	{
		// Calculate a random position within the radius.
		// The distribution below gives a good random sampling
		// This should be pre-calculated
		float rad = random01() * radius;
		float a = random01() * TWOPI;
		float b = random01() * TWOPI;
		Point3 vector;
		vector.x = rad * Sin( a ) * Cos( b );
		vector.y = rad * Sin( a ) * Sin( b );
		vector.z = rad * Cos( a );

		// Get sample point, in camera space
		Point3 samplePoint = p + vector;

		// Get screen coordinate of the sample point
		Point2 screenSamplePoint = MapCamToScreen( samplePoint );
		int sx = int( screenSamplePoint.x + 0.5f );
		int sy = int( screenSamplePoint.y + 0.5f );

		// If sample point is outside the bitmap then ignore it.
		// This code could potentially be skipped in a realtime scenario
		if( sx < 0 || sy < 0 || sx >= w || sy >= h )
			continue;

		// Get z-buffer depth at sample point
		float sampleZ = zBuffer[ sx, sy ];
		// Do nothing with samples that are the background
		if( sampleZ <= -1.0E30f || sampleZ == 0.0f )
			return;

		// Get the difference of the depth at the sample and the depth at p
		float zd = sampleZ - z;

		// Ignore samples with a depth outside the radius or further away than p
		if( zd < radius )
		{
			// Calculate difference in distance to sample point and the z depth at that point
			// Optimized by using squared, ok due to the nature of how we will use it
			// One could probably use samplePoint.z instead of length though.
			float zd2 = LengthSquared( samplePoint ) - sampleZ*sampleZ;

			// Check that the sample point is in front of the z-buffer depth at that point
			if( zd2 > 0.0f )
			{
				// Now get a new point that is samplePoint, but with an adjusted z depth
				Point3 p2 = Normalize( samplePoint ) * -sampleZ;

				// Get cosine of angle between the normal in p and a vector from p to p2
				float dp = DotProd( -normal, Normalize( p2 - p ) );
				// Check that the angle is inside the cone angle
				if( dp > coneAngle )
					occlusion += 1.0f;
			}
		}
		actualSamples++;
	}

	// Calculate final occlusion.
	// Multiply by 2 since we roughly lose quite a lot of samples with the normal check
	occlusion *= 2.0f * strength / float( actualSamples );
	LimitValue( occlusion, 0.0f, 1.0f );
}