# Inconsistent shading in pathtraced image

I am currently building a pathtracer, which has support for vertex normals and face normals.

There seems to be an issue when it comes to rendering objects with triangles exported from Blender. Regardless of how I calculate the normal, the result always shows inconsistent shading of triangles.

The shading seems to change per triangle, with the ones more aligned to the camera being correct. It seems to depend on where the objects are in the scene relative to the camera. Below is the intersection algorithm for the triangle.

bool Triangle::intersection(Ray &ray, HitInfo &hit)
{
float3 edge1 = v1->pos - v0->pos;
float3 edge2 = v2->pos - v0->pos;

float3 h = ray.getDirection().cross(edge2);
float a = edge1.dot(h);

float f = 1.0 / a;
float3 s = ray.getOrigin() - v0->pos;
float u = f * (s.dot(h));

if (u < 0.0 || u > 1.0)
return false;

float3 q = s.cross(edge1);
float v = f * ray.getDirection().dot(q);
if (v < 0.0 || u + v > 1.0)
return false;

float t = edge2.dot(q) * f;
if (t > 1e-9 && t < ray.tmin)
{
hit.t = t;
float3 temp_normal = unit(edge2.cross(edge1)); //unit(v1->normal*u + v2->normal*v + v0->normal*(1. - u - v));
if (unit(ray.getDirection()).dot(temp_normal)< 1e-9) temp_normal = temp_normal * (-1);
hit.normal = temp_normal;
return true;
}
else
{
return false;
}
}


The intersection algorithm calculate the normal using the cross product of the edges, however using the normals provided by blender does not change much either. I use the Moller Trumbore intersection algorithm, exporting obj's from Blender (enabling "keep vertex order" doesn't change anything). I have also tried different materials but the problem still persists.

The above image is the normals being rendered with an emissive shader. This is the result I would expect, so I don't think this is the issue.

float3 cosineSampleHemi(float u1, float u2)
{
float z = std::pow(1.0 - u1, 3.0 / 5.0);
float phi = 2.0 * M_PI * u2;
float theta = sqrt(std::max(0.0, 1.0 - z * z));
float x = theta * cos(phi);
float y = theta * sin(phi);

return float3(x, y, z);
}

float3 random_in_unit_sphere() {
float3 p;
do {
p = float3(drand48(),drand48(),drand48())*2.0 - float3(1.0);
} while (p.dot(p) >= 1.0);
return p;
}


The above code is for the sampling, I have tried both functions and it produces similar results. Below is the lambertian scatter function, which uses the hemisphere sampling methods.

bool Lambertian::scatter(Ray &ray, HitInfo &hit, float3 &attenuation, Ray &new_ray) const
{
new_ray = Ray(ray.getHit(hit.t), cosineSampleHemi(drand48(), drand48()));
attenuation = m_colour;
return true;
}


If any more info is needed I will be happy to supply it, as I am completely stumped at this point.

• The look of the gradients in your first image looks like swapped normals to me. Have you tried rendering your normals to check that they are correct? My suspicion would be directed toward vertex indices. – Julien Guertault Jan 31 '18 at 2:21
• I have updated my answer to include an image of the normals, and it seems to be what I would expect. Or rather, the normals are consistent on the triangles that make up a plane. – Oliver Cruickshank Jan 31 '18 at 9:39
• I don't think the problem is in the code you've shown. It looks more like the light isn't being transformed into the coordinate system of the object correctly. Where's the code that shades the pixel based on the normal? Does it perform the N·L calculation in world space or object space? (Transform light to object space or transform object to light space, or transform both to world space?) – Wyck Jan 31 '18 at 15:47
• The lighting is done by sampling a unit sphere or hemisphere. I just use the normal as calculated by the cross product of the edges or the normals from blender. I'll link the code that samples the hemisphere. – Oliver Cruickshank Jan 31 '18 at 16:10

I have managed to fix the error. It turns out the error was never situated in the normal calulation it was the shading algorith. The floating point precision error caused the new ray to be slightly below the surface, which meant it would intersect itself and become darker. This is a problem common in biased raytracers, the phenomenon known as "Shadow acne".

To fix it I added the following code:

bool Lambertian::scatter(Ray &ray, HitInfo &hit, float3 &attenuation, Ray &new_ray) const
{
float3 offset = hit.normal*(1e-5);
new_ray = Ray(ray.getHit(hit.t) + offset, cosineSampleHemi(drand48(), drand48()));
attenuation = m_colour;
return true;
}


the offset variable moves the ray origin to slightly above the surface by a small amount and that seems to fix the issue.

• The problem is common to all ray based rendering in general for what that's worth! – Alan Wolfe Jan 31 '18 at 18:45
• Would backface culling be a solution to this? – PaulHK Feb 12 '18 at 6:02

Your normal calculation looks backwards: edge2.cross(edge1), but (assuming CCW winding) it should be edge1 × edge2.

The sign flip for back facing normals is also reversed; the ray direction and triangle normal should be facing in opposite directions, thus should have negative dot product, but it looks like you're flipping it to make the dot product always positive.

Maybe there's another differing convention elsewhere in your shading code that accounts for this? In any case, I'm not sure this is actually related to your problem, but it jumped out at me as being odd.

• Yeah I should have pointed out I've tried doing edge1.cross(egde2) without any noticeable difference. The dot product of the incident ray and the normal also does not seem to have a noticeable effect when I change the comparison sign or just remove it completely. – Oliver Cruickshank Jan 30 '18 at 21:34