1
$\begingroup$

Spheres with shadows work fine. But when I try to include triangles with shadows, they are shaded incorrectly (i.e completely black):

enter image description here

But when I disable shadows Blinn shading on triangles work fine (comment lines 128, 129, 130 in main.py file attached below):

enter image description here

I am following this article for triangle / ray intersection.

main.py:


import sys
from PIL import Image
from math import * 
from util import *


width  = 600
height = 400


#-----------------------------------------------------------------------

def GetNearestIntersection(objects, ray):
    nearestObj = None
    minHit = Intersection(None, INF, None, None)
    
    for obj in objects:
        hit = obj.intersect(ray)
        if hit.distance < minHit.distance:
            nearestObj = obj
            minHit = hit
            
    return minHit


def Blinn(light, hit, eye):
    ka = hit.object.material.ambient
    kd = hit.object.material.diffuse
    ks = hit.object.material.specular
    la = light.material.ambient
    ld = light.material.diffuse
    ls = light.material.specular
    L  = (light.position - hit.point).normalize()
    N  = hit.normal
    V  = (eye - hit.point).normalize()
    a  = hit.object.material.shininess
    
    A = ka*la
    D = kd*ld*L.dot(N)
    S = ks*ls*pow(N.dot((L+V).normalize()) ,a/4)  
    p = A + D + S
    
    return p


#-----------------------------------------------------------------------

def main():
    global bitmap
    
    eye = Vector(0,0,1)
    ratio = float(width) / height
    screen = Screen(-1, 1 / ratio, 1, -1 / ratio, 0)
    
    objects = []
    objects.append(Sphere(Vector(-0.2,0,-1),  0.7,      Material(Vector(0.1, 0, 0),Vector(1,0,0),Vector(1,1,1), 100, 0.5, -1)))
    #objects.append(Sphere(Vector( 0.1,-0.3,0),0.1,      Material(Vector(0,0,0),Vector(0,1,0),Vector(1,1,1), 100, 0.2, 1.5)))
    #objects.append(Sphere(Vector(-0.3,0.15,0),0.15,     Material(Vector(0,0,0),Vector(0,0,1),Vector(1,1,1), 100, 0.8, -1)))

    objects.append(Sphere(Vector(0,-9000,0),  9000-0.7, Material(Vector(0,0,0),Vector(0.71,0.71,0.71),Vector(0,0,0), 100, 0.01, -1)))   

    #cube = Mesh(Material(Vector(0.1, 0, 0),Vector(1,0,0),Vector(1,1,1), 100, 0.5, -1));
    #cube.load("cube3.obj")
    #print("Object loaded")

    #objects.append(cube)

    t1 = Triangle((Vector(2.001539, 2.006154, -0.2), Vector(2.001539, 0.006154, -0.2), Vector(0.001539, 0.006154, -0.4)), (Vector(0, 0, 1), Vector(0, 0, 1), Vector(0, 0, 1)), Material(Vector(0.1, 0, 0),Vector(1,0,0),Vector(1,1,1), 100, 0.5, -1))
    
    #t2 = Triangle((Vector(2.001539, -0.003401, -2.006154), Vector(2.001539, -0.003401, -0.006154), Vector(0.001539, -0.003401, -0.006154)), (Vector(0.0000, -1.0000, 0.0000), Vector(0.0000, -1.0000, 0.0000), Vector(0.0000, -1.0000, 0.0000)), Material(Vector(0.1, 0, 0),Vector(1,0,0),Vector(1,1,1), 100, 0.5, -1))
    
    objects.append(t1)
    #objects.append(t2)

    light = Light(Vector(5,5,5), Material(Vector(1,1,1),Vector(1,1,1),Vector(1,1,1), -1, -1))

    
    for frame in range(1):
    
        img = Image.new(mode = "RGB", size=(width, height), color=Color.WHITE)
        bitmap = img.load() # create the pixel data

#--------------------------------------------------------------
#---                    YOUR CODE HERE                      ---
#--------------------------------------------------------------
        sys.setrecursionlimit(10000)
        
        #breakpoint()
        
        deltaX = (screen.right - screen.left)/(width-1)
        deltaY = (screen.top - screen.bottom)/(height-1)

        for y in range(height):
            for x in range(width):
                pixel = Vector(screen.left+x*deltaX, screen.top-y*deltaY, screen.z)
                direction = (pixel - eye).normalize()
                pixelRay = Ray(eye, direction)
    
                bitmap[x,y] = Color.WHITE
                
                hit = GetNearestIntersection(objects, pixelRay)
                if hit.point != None:
                    #bcolor = hit.object.material.diffuse
                    
                    bcolor = Blinn(light, hit, eye)
                    
                    color = (int(bcolor.x*256), int(bcolor.y*256), int(bcolor.z*256))
                    
                    #  Shoot extra rays:
                    #1) Shadow Ray:
                    shadowed = False
                    shiftedPoint = hit.point + hit.normal * 1e-5
                    #shiftedPoint = Vector(hit.point.x,hit.point.y, 0.01)
                    #print(hit.point.toString() + "  --  " + shiftedPoint.toString())
                    shadowRay = Ray(shiftedPoint, (light.position - shiftedPoint).normalize())
                    result = GetNearestIntersection(objects, shadowRay)
                    #print(result.point.toString())
                    if result.point != None:
                    #    print(result.object.name)
                        shadowed = True
                    
                    #2) Reflection Rays:
                    reflectionBounces = 3
                    for i in range(reflectionBounces):
                        pass
                    
                    #if(shadowed):
                    #    bitmap[x,y] = Color.BLACK
                    #else:
                        bitmap[x,y] = color
                                
            print("progress: %d %%" % ((y+1)/height*100.0))
#--------------------------------------------------------------
#--------------------------------------------------------------
#--------------------------------------------------------------

        img.show()
        img.save("pic1.png")
        #img.save("images/fig" + f'{frame:06}' + ".png")
        #print("Saving ---> images/fig" + f'{frame:06}' + ".png")
        #img.close()

main()

util.py:


from math import * 

PI  = 3.14159265
INF = 9999999999
#-----------------------------------------------------------------------

class Color:
    BLACK = (0,0,0)
    RED   = (245,23,32)
    BLUE  = (46,139,192)
    GREEN = (24,165,88)
    PINK  = (250,38,160)
    GOLD  = (248,210,16)
    TEAL  = (43,124,133)
    WHITE = (255,255,255)
    CHROMA= (0,255,0)
    
#-----------------------------------------------------------------------

def dot(a, b):
    return a.x*b.x + a.y*b.y + a.z*b.z

def normalize(a):
    mag = a.magnitude()
    return Vector(a.x/mag,a.y/mag,a.z/mag)

class Vector:
    def __init__(self,x,y,z):
        self.x = x
        self.y = y
        self.z = z
        self.w = 1

    def toString(self):
        return "S:["+"{:.3f}".format(self.x)+", "+"{:.3f}".format(self.y)+", "+"{:.3f}".format(self.z)+"]"

    def dot(self, b):
        return self.x*b.x + self.y*b.y + self.z*b.z
    
    def cross(self, b):
        return Vector(self.y*b.z-self.z*b.y, self.z*b.x-self.x*b.z, self.x*b.y-self.y*b.x)
        
    def magnitude(self):
        return sqrt(self.x*self.x + self.y*self.y + self.z*self.z)

    def normalize(self):
        mag = self.magnitude()
        return Vector(self.x/mag, self.y/mag, self.z/mag)

    # Provide "overridden methods via the "__operation__" notation; allows you to do, for example, a+b, a-b, a*b
    def __add__(self, b):
        return Vector(self.x + b.x, self.y+b.y, self.z+b.z)

    def __sub__(self, b):
        return Vector(self.x-b.x, self.y-b.y, self.z-b.z)

    def __mul__(self, b):
        if type(b) == float or type(b) == int:
            return Vector(self.x*b, self.y*b, self.z*b)
        elif type(b) == Vector:
            return Vector(self.x*b.x, self.y*b.y, self.z*b.z)
        else:
            print(type(b))
            assert False


class Material:
    def __init__(self, ambient, diffuse, specular, shininess = 0, reflection = 0, refractiveIndex = -1):
        self.ambient    = ambient               # Vector(r,g,b)
        self.diffuse    = diffuse               # Vector(r,g,b)
        self.specular   = specular              # Vector(r,g,b)
        self.shininess  = shininess             # Integer between 0 and 100
        self.reflection = reflection            # Real number between 0 and 1.0
        self.refractiveIndex = refractiveIndex  # Real number. -1 means it is a solid object


class Intersectable:
    def intersect(self, ray):
        assert False, "Error: You must override this method"

class Sphere(Intersectable):
    def __init__(self,center, radius, material):
        self.center   = center
        self.radius   = radius
        self.material = material
        
    def intersect(self, ray):
        OC = ray.origin - self.center
        a = ray.direction.dot(ray.direction)
        b = 2.0 * ray.direction.dot(OC)
        c = OC.dot(OC) - self.radius*self.radius
        descriminant = b*b - 4*a*c
        if(descriminant > 0):
            t1 = (-b + sqrt(descriminant))/2
            t2 = (-b - sqrt(descriminant))/2
            if t1 > 0 and t2 > 0:
                dist = min(t1, t2)
                point = ray.pointAtParameter(dist)
                normal = (point - self.center).normalize()
                return Intersection(point, dist, normal, self)
        return Intersection(None, INF, None, None)
        
                
    def normal(self, b):
        return (b - self.center).normalize()


class Ray:
    def __init__(self, origin, direction):
        self.origin    = origin
        self.direction = direction

    def pointAtParameter(self, t):
        return self.origin + self.direction*t
    
    def toString(self):
        return "R->{ "+ self.origin.toString() + " -- " + self.direction.toString() +" }"
       
class Intersection:
    def __init__(self, point, distance, normal, object):
        self.point    = point
        self.distance = distance
        self.normal   = normal
        self.object   = object
        
        
class Screen:
    def __init__(self,  left, top, right, bottom, z):
        self.left   = left
        self.top    = top
        self.right  = right
        self.bottom = bottom
        self.z      = z
        
        
class Light:
    def __init__(self, position, material):
        self.position = position
        self.material = material

class Triangle(Intersectable):
    def __init__(self, verts, normals, material):
        self.verts     = verts     # tuple of 3 vertices
        self.normals   = normals   # tuple of 3 normals
        self.material  = material
        self.name      = "triangle"
   
    def intersect(self, ray):
        A = self.verts[0]
        B = self.verts[1]
        C = self.verts[2]
        
        # Compute normal Vector:
        num    = (B-A).cross(C-A)
        den    = num.magnitude()
        invDen = 1.0/den
        n = (num * invDen).normalize()
        
        d = ray.direction
        P = ray.origin
        
        dconst = n.dot(A)
        t = (dconst - n.dot(P)) * (1.0/(n.dot(d)))
        
        Q = ray.pointAtParameter(t)

        if ((B-A).cross(Q-A)).dot(n) < 0:
            return Intersection(None, INF, None, None)
        if ((C-B).cross(Q-B)).dot(n) < 0:
            return Intersection(None, INF, None, None)
        if ((A-C).cross(Q-C)).dot(n) < 0:
            return Intersection(None, INF, None, None)
        
        dist = t
        normal = self.normals[0]
        point = Q
        
        return Intersection(point, dist, normal, self)
    

class Mesh(Intersectable):
    def __init__(self, material):
        self.material = material
        self.vertices = []      # each vertex is a Vector3D
        self.normals  = []      # each normal is a Vector3D
        self.faces    = []      # each face is a list of 3 pairs [ (v, vn), (v, vn), (v, vn) ]
        
    def load(self, fileName):
        fo = open(fileName, "r+")
        
        lines = fo.readlines()
        
        for i in range(len(lines)):
            prefix = lines[i][0:2]
            if prefix == 'v ':
                v = lines[i].split()
                self.vertices.append(Vector(float(v[1]), float(v[2]), float(v[3])))
            elif prefix == 'vn':
                vn = lines[i].split()
                self.normals.append(Vector(float(vn[1]), float(vn[2]), float(vn[3])))
            elif prefix == 'f ':
                f = lines[i].split()
                triangle = []

                for j in range(1,4):
                    vindex = int(f[j].split('/')[0]) - 1
                    nindex = int(f[j].split('/')[2]) - 1
                    pair = (vindex, nindex)
                    triangle.append(pair)

                self.faces.append(triangle)
                
               #v1 = self.vertices[vindex]
               #n1 = self.normals[nindex]
            
               # vindex = float(f[2].split('/')[0])
               # nindex = float(f[2].split('/')[2])
               # v2 = self.vertices[vindex]
               # n2 = self.normals[nindex]

               # vindex = float(f[3].split('/')[0])
               # nindex = float(f[3].split('/')[2])
               # v3 = self.vertices[vindex]
               # n3 = self.normals[nindex]

               # triangle = Triangle()                    

        fo.close()

    def intersect(self, ray):
        
        for f in self.faces:
            v1 = self.vertices[f[0][0]]
            n1 = self.vertices[f[0][1]]
            v2 = self.vertices[f[1][0]]
            n2 = self.vertices[f[1][1]]
            v3 = self.vertices[f[2][0]]
            n3 = self.vertices[f[2][1]]
            
            trig = Triangle((v1, v2, v3), (n1, n2, n3), self.material)
            
            hit = trig.intersect(ray)
            
            if(hit.point != None):
                return hit
                
        return Intersection(None, INF, None, None)
        
$\endgroup$
2
  • 2
    $\begingroup$ I've only skimmed though your code, but regarding "shiftedPoint = hit.point + hit.normal * 1e-5" have you checked the length of the normal being returned from the triangle and/or tried increasing the 1e-5 "epsilon" value? You might be getting self shadowing. $\endgroup$
    – Simon F
    Jul 1 at 8:17
  • $\begingroup$ Perhaps your triangle's vertices are winded the wrong way around and your normals are pointing in the wrong direction. Does the issue persist if you rotate the camera and light 180° around the scene? Also try negating the triangle's normal. $\endgroup$
    – AnnoyinC
    Jul 12 at 8:33

Your Answer

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

Browse other questions tagged or ask your own question.