0
$\begingroup$

I am writing a small raytracer in python , everything was working fine but when i implemented plane geometry then the reflective spheres are getting weird patches on lower bottom. image below:

I guess this is happening because of some error in calculating lighting with wrong normals? but i am unable to figure out exactly what. enter image description here

code to find palne-ray intersection:

class Plane(Shape):
    def __init__(self, color, y=0, specular=-1, reflective=0):
        self.specular = specular
        self.reflective = reflective
        self.center = Vec3(0,y,0)
        self.normal = Vec3(0,-1,0)
        self.color = color

    def intersect_at_point(self, origin, ray):
        denom = ray.dot(self.normal)

        if abs(denom) > 0.0001:
            diff  = self.center - ray
            t = diff.dot(self.normal) / denom

            if t > 0.0001:
                return t
        return None

trace ray function.

def trace_ray(self, origin, direction, t_min, t_max, depth):
    closest_object, closest_t = self.closest_intersection(origin, direction, t_min, t_max)

    if closest_object == None:
        return Vec3(173/255, 216/255, 230/255)

    # Compute local color
    P = origin + closest_t * direction  # Compute intersection
    N = P - closest_object.center  # Compute  normal at intersection
    N = N / N.mag()
    local_color = closest_object.color * self.compute_light(P, N)

    # If we hit the recursion limit or the object is not reflective, we're done
    r = closest_object.reflective
    if depth <= 0 or r <= 0:
        return local_color

    # Compute the reflected color
    R = self.reflect_ray(-1*direction, N)
    reflected_color = self.trace_ray(P, R, 0.001, math.inf, depth - 1)

    return local_color * (1 - r) + reflected_color * r

closest intersection method.

def closest_intersection(self, O, D, t_min, t_max):
    closest_t = math.inf
    closest_object = None

    for object in self.world.objects:
        t1, t2 = object.intersect_at_point(O, D)

        if (t_min < t1 < t_max) and t1 < closest_t:
            closest_t = t1
            closest_object = object

        if (t_min < t2 < t_max) and t2 < closest_t:
            closest_t = t2
            closest_object = object

    for plane in self.world.planes:
        t = plane.intersect_at_point(O,D)

        if t and (t_min < t < t_max) and t < closest_t:
            closest_t = t
            closest_object = plane

    return closest_object, closest_t

Calculate light method.

def compute_light(self, Point, Normal):
    i = 0.0
    P = Point
    N = Normal

    for light in self.world.lights:
        if light.type == 'ambient':
            i += light.intensity
        else:
            if light.type == 'point':
                L = light.position - P
                t_max = 1

            if light.type == 'directional':
                L = light.direction
                t_max = math.inf

            # Shadow check
            shadow_sphere, shadow_t = self.closest_intersection(P, L, 0.001, t_max)
            if shadow_sphere != None:
                continue

            n_dot_l = N.dot(L)

            if n_dot_l > 0:
                # print(f'light intensity {light.intensity}')
                i+= light.intensity * n_dot_l / (N.mag() * L.mag())

    return i
$\endgroup$
2
$\begingroup$

I have to disagree with @jack11111, this is definitely not a clipping issue. I have two possible explanations which you can easily test and verify.

Clipping theory

  • The patch looks pretty constant in color, clipping would be sort of a gradient (modulo 255) of full intensity in at least one channel (R/G/B).
  • There is no direct light source to cause a strong specular highlight. According to the ground shading and the Lambert-shaded green balls a point light source is placed above, little bit in front of the camera. It seems to have normal intensity that should not cause any trouble. In fact I see in your code that you apply only basic Lambert n . l shading, so no specular highlights possible.
  • There is no indirect source to cause such a peak. The rays reflect to towards ground that is rather dark. The bright spot on the ground is located else where. That is well visible on the red sphere where the error occurs closer than the next green sphere, but the highlight on the ground is even further away.
  • It is a strange coincidence that the secondary color of the patch is exactly the same for both spheres.

Normals theory

  • Normals seem to be computed correctly elsewhere, all your shading is consistent with nice gradients.
  • You reflections are also correct elsewhere, that will be on problem.

It's a trap! (almost)

Looking closely at your image I believe I can explain what is happening there. The rays hitting the suspicious regions at the sphere will reflect towards ground in a direction very similar to the ground normal.

You ground does not seem to be reflective, but if it were: The rays would reflect back in almost the exact opposite direction hitting back the ball and then heading towards the camera. So they would hit the ball twice which could cause its color to gain dominance. You can test this theory by comparing the rendering results with maxDepth = 1. If the discontinuity disappears then that is the problem.

Upside down

My second explanation is much more likely to be the case. It is also based on the observation that the rays reflected from the suspicious patches hit the ground in a direction pointing almost exactly down. I noticed a bit strange setup of your plane in your code. It has its normal pointing down!?

self.normal = Vec3(0,-1,0)

I am not sure how the Lambert shading of the ground works then (should be then all just ambient in fact), but assuming Vec(0,1,0) is your up then the intersection will in fact yield None

denom = ray.dot(self.normal) #something like 1e-6 in the suspicious area
if abs(denom) > 0.0001: #evaluates to false
    #... true branch ignored
return None #false branch activated, returns no intersection

Having no intersection means that the hit point is shaded only with a constant color

if closest_object == None:
   return Vec3(173/255, 216/255, 230/255)

So to test this explanation, just change the returned color for this case to black and see if the patch darkens.

if closest_object == None:
    return Vec3(0, 0, 0)

I hope that helps. Please let me know the results.

Update

After the author confirmed my second explanation I found the reason in the code. The source of the problem is in the ray-plane intersection method.

Most important of all the epsilon in the comparison should be omitted, since that is causing the patch to be discontinuous. As mentioned earlier, the incoming ray direction is almost Vec3(0,-1,0). You have set your plane normal to exactly Vec3(0,-1,0) so the dot product yields something very close to 0.

So the quick fix would be:

denom = ray.dot(self.normal)
if abs(denom) >= 0: #omit the epsilon
    #...

Your plane normal is inconsistent with how a normal is usually defined. I believe your plane is not supposed to face down. It would be much better and correct to have the plane normal pointing up and then testing for a negative dot product.

A front-face hit occurs when the face normal and the ray are pointing in opposite directions, i.e. their angle θ is smaller than -π/2 or larger than π/2, so cos(θ) < 0. Note that denom is in fact cos(θ). If |θ| < π/2, i.e. cos(θ) > 0, then it is a back-face hit. You ignore such hits for now but once you add transparency to your raycaster you will need them as well.

The correct way would be:

def __init__(self, color, y=0, specular=-1, reflective=0):
    # ...
    self.normal = Vec3(0,1,0) #pointing UP

    def intersect_at_point(self, origin, ray):
        denom = ray.dot(self.normal) # denom == cos(θ)

        if abs(denom) <= 0: #is negative <=> |θ| > π/2; omit the epsilon
            diff  = self.center - ray
            t = diff.dot(self.normal) / denom

            if t > 0: #I suggest to omit the epsilon here as well
                return t
        return None
| improve this answer | |
$\endgroup$
  • $\begingroup$ wow!! I cannot thank you enough as I was banging my head for the past 2 days because of this issue, it happened to be what you said the upside-down I guess. when I changed the background color to black the patches became black too. I guess i need to study more on normals & axis orientations I will update my code with more understanding in a few days and accept the answer if that's ok with you? anyways thanks a ton really :) $\endgroup$ – anekix May 14 at 22:51
  • $\begingroup$ Glad to hear that! Oh and now I see what you are doing wrong in the plane! I will add that to my answer. $\endgroup$ – Isolin May 14 at 22:59
  • $\begingroup$ my y is -ve upwards, even then I changed the plane to Vec3(0,1,0) the black patch remains. I think I will rewrite the code with the correct axes. This is my first program in graphics programming so a bit confused with something that should be very obvious. $\endgroup$ – anekix May 14 at 23:04
  • $\begingroup$ Nope, the reason is the epsilon in the denom check. Changing the normal is just for correctness and then the denom check needs to be inverted as well. $\endgroup$ – Isolin May 14 at 23:30
0
$\begingroup$

It looks like the light and colour calculation for the sphere reflection is peaking and may need to be clipped to the range 0-255. This video around 1:21:45 mark shows an example of this.

| improve this answer | |
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.