War on noise

The latest version of Wooscripter has just been released and there are some major improvements to the raytracer to support lower noise final renderings with far smoother early results.

I’ve already covered a fair amount of the foundational work for 2d samplers in this earlier article, but I haven’t talked about the changes to the sphere light and the world light so let’s cover those first.

The world light is used in the default daylight and sunset lighting scripts. The basic idea is to emulate global lighting without going as far as a full pathtracer. See the following renders that show the difference between a point light and a world light.

Cube of cubes + World light

Cube of cubes + World light

Cube of cubes + Directional light

Cube of cubes + Directional light

As you can see world lighting doesn’t result in many harsh shadows but it does give nice darkening in folds and around concave areas of shapes. This gives a more realistic looking render than a vanilla point light at the expense of greatly increased render time. The increased render time occurs because we need to sample the world light in many different directions to get a good estimate for the amount of sky that is visible from a single point.

At the moment each pass of the raytracer evaluates four separate directions so that the noise associated with world lighting decreases more rapidly than other sources of noise in the image. The algorithm used is fairly naive with multiple directions generated that point away from the surface. These rays are then compared to a cosine distribution and rejected if they’re too far from this distribution. This is a form of Monte Carlo rendering which should lead to a faster good estimate at the expense of early noise in the rendering algorithm. It’s that early noise that causes the very noisy initial renders that you get in the preview window.

So rather than doing a monte carlo sampling of a cosine distribution I’ve switched to using a biased cosine sampler instead. The code for this is borrowed slightly cheekily from Syntopia’s recent article about path tracing. The code for the biased sampler is included in Syntopia’s article and worked without any major modification inside my own raytracer. The immediate result of this change is to radically reduce the noisiness of the world lighting for low number of samples.

Next up is the sphere light. There were a few aspects of the sphere light that lead to high noise. When I wrote the original lighting algorith, I tried to be very accurate in the light contribution from the sphere light by sampling the sphere and only including light contributions for surface points which are visible to the incoming ray.

To do this I generate a random Vec3, and then orientated this towards the ray. I then find the collisionpoint for a ray from the object to this surface point. If this doesn’t hit an object I check that this surfaceposition is pointing towards the origin of the light ray. If not I discard and resample.

// generate random vector3
DVector3 surfacePosition = DVector3(rand.GetRandom(), rand.GetRandom(), rand.GetRandom());

// if it points away from ray, flip it over
if (surfacePosition.GetDot(ray.GetDirection)<0) surfacePosition = -surfacePosition;

// if the ray still can't see this point, discard, increase sample count, try again
if (surfacePosition.GetDot(collisionPoint - ray.GetStart)<0)
// then sample the light again (control structure of your choice)

// when we've got 4 hits
out_colour = total_colour / samples;

The problem is that as you get close to the sphere the effective area of the sphere which illuminates a point decreases considerably. This meant that I had to generate many more points (and cast many more rays) to get a fixed number of light samples. At a distance from a sphere this just caused the occasional point to fail and get resampled leading to a half-weighted lighting contribution (yet again, more noise).

So now I'm not trying to be clever. If the surfaceposition is effectively invisible from the origin of the test ray, I just include it anyway. Slightly less accurate result (which you'll never notice anyway) and vastly reduced noise. Win!

The light attenutation for the sphere light has also been changed from a rather perfect pathtraced solution which generate high noise at near distances, to one which uses a simple light attenuation model as proposed by Tom Madams in this handy little post. Again this serves to reduce noise when sphere lights are close to surfaces and leads to more satisfying renders with fewer samples.

The final easy win I've done is to change many of my algorithms to use coherent 2d samplers. For things like glossy reflections I used to use a random 3d vector to jitter the sample direction. I've now changed this to use a 2d vector which is converted into a hemispherical normal vector.

DVector3 DScene::GetRandomDirection3d(const DRayContext &rayContext) const
	DVector2 random2d = GetRandom2D(rayContext);
	float azimuth = random2d.x * 2 * 3.14159265f;
	DVector2 dir2d = DVector2(cosf(azimuth), sinf(azimuth));
	float z = (2*random2d.y) - 1;
	DVector2 planar = dir2d * sqrt(1-z*z);
	return DVector3(planar.x, planar.y, z);

Apologies but I've forgotten the exact source for this wonderful piece of code, but it basically guarantees an even distribution of random samples across the 3d hemisphere using only a 2d vector. Sweet!

So I'm going to leave this roundup of improvements to sphere lighting etc. with a quick little render of a bunch of cubes lit by a huge sphere just offscreen! This still has a bit of noise, but I only left it to render for a couple of minutes!

Low noise improvements in action

Low noise improvements in action

Low noise improvements in action 2

Low noise improvements in action 2

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *

Spam Protection *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>