Exemple #1
0
class TestRenderer(unittest.TestCase):

    @classmethod
    def setUpClass(self):
        self.shapes = BasicShapes(10, 10)
        self.line1 = self.shapes.drawLine(0, 5, 9, 5, (255, 0, 0))
        self.line2 = self.shapes.drawLine(0, 5, 9, 5, (0, 255, 0))
        self.line3 = self.shapes.drawLine(5, 0, 5, 9, (0, 0, 255))

    def setUp(self):
        self.renderer = Renderer(10, 10)

    def test_interface(self):
        for func in ('render', 'clearBuffer', 'setBackgroundColor', 'drawOver', 'drawUnder'):
            self.assertTrue(hasattr(self.renderer, func) and callable(getattr(self.renderer, func)), "Interface requires function: " + func)

    def test_render(self):
        self.renderer.setBackgroundColor(200, 200, 200)
        self.renderer.clearBuffer()
        self.renderer.drawOver(self.line1)
        self.renderer.drawUnder(self.line2, 3, 3)

        render = self.renderer.render()
        self.assertIs(type(render), list)
        self.assertEqual(render[5], [255, 0, 0]*10, "The red line was not rendered properly")
        self.assertEqual(render[8], [200, 200, 200]*3 + [0, 255, 0]*7, "The green line was not rendered properly")
        for row in [0, 1, 2, 3, 4, 6, 7, 9]:
            self.assertEqual(render[row], [200, 200, 200]*10, "Row " + str(row) + " did not render background properly")

        self.renderer.render('./test.png')
        self.assertTrue(os.path.exists('./test.png'), "The file wasn't created upon rendering")
        try:
            os.remove('./test.png')
        except OSError:
            fail("Could not clean up the created test file")

    def test_clearBuffer(self):
        self.renderer.setBackgroundColor(10, 20, 30)
        self.renderer.clearBuffer()
        for i, row in enumerate(self.renderer.pixels):
            self.assertEqual(row[0:30], [10, 20, 30]*10, "Row " + str(i) + " was not cleared")

    def test_setBackgroundColor(self):
        self.assertEqual(self.renderer.setBackgroundColor(200, 200, 200), [200, 200, 200])
        self.assertEqual(self.renderer.setBackgroundColor(256, 255, 255), [200, 200, 200])
        self.assertEqual(self.renderer.setBackgroundColor(), [200, 200, 200])
        self.assertEqual(self.renderer.setBackgroundColor(200, 100), [200, 200, 200])
        self.assertEqual(self.renderer.setBackgroundColor(200, 100, 50), [200, 100, 50])

    def test_drawOver(self):
        self.renderer.drawOver(self.line1)
        self.assertEqual(self.renderer.pixels[5], [255, 0, 0]*10)

        self.renderer.drawOver(self.line1, 3, 2)
        self.assertEqual(self.renderer.pixels[7][9:30], [255, 0, 0]*7)

        self.renderer.drawOver(self.line2, 2, 0)
        self.assertEqual(self.renderer.pixels[5], [255, 0, 0]*2 + [0, 255, 0]*8)

    def test_drawUnder(self):
        self.renderer.drawUnder(self.line1)
        self.assertEqual(self.renderer.pixels[5], [255, 0, 0]*10)

        self.renderer.drawUnder(self.line1, 3, 2)
        self.assertEqual(self.renderer.pixels[7][9:30], [255, 0, 0]*7)

        self.renderer.drawUnder(self.line2, 2, 0)
        self.assertEqual(self.renderer.pixels[5], [255, 0, 0]*10)

    def test_integration_drawOrder(self):
        self.renderer.drawOver(self.line1, 0, -2)
        self.renderer.setBackgroundColor(100, 100, 100)
        self.renderer.clearBuffer()
        self.renderer.drawOver(self.line1)
        self.renderer.drawUnder(self.line2, 1, 3)
        self.renderer.drawOver(self.line3)
        self.renderer.drawUnder(self.line3, 2)
        render = self.renderer.render()

        self.renderer.render('./integration_drawOrder.png')
        self.assertTrue(os.path.exists('./integration_drawOrder.png'), "The file wasn't created upon rendering")

        self.assertIs(type(render), list)
        for row in [0, 1, 2, 3, 4, 6, 7, 9]:
            self.assertEqual(render[row], [100, 100, 100]*5 + [0, 0, 255] + [100, 100, 100] + [0, 0, 255] + [100, 100, 100]*2,
                    "Background did not render properly on row: " + str(row))
        self.assertEqual(render[5], [255, 0, 0]*5 + [0, 0, 255] + [255, 0, 0]*4, "Redline did not render properly")
        self.assertEqual(render[8], [100, 100, 100] + [0, 255, 0]*4 + [0, 0, 255] + [0, 255, 0]*4, "GreenLine did not render properly")
Exemple #2
0
class TestSimpleTurtle(unittest.TestCase):

    @classmethod
    def setUpClass(self):
        pass

    def setUp(self):
        self.renderer = Renderer(20, 20)
        self.turtle = SimpleTurtle(10, 19, 90, self.renderer)

    def test_interface(self):
        for func in ('turnRight', 'turnLeft', 'push', 'pop', 'reset', 'draw', 'setAngle', 'setRandomAngle', 'turnLeftRandom', 'turnRightRandom'):
            self.assertTrue(hasattr(self.turtle, func) and callable(getattr(self.turtle, func)), "Interface requires function: " + func)

    def test_turnRight(self):
        self.assertEqual(self.turtle.turnRight(), 60)
        self.assertEqual(self.turtle.turnRight(), 30)

    def test_turnLeft(self):
        self.assertEqual(self.turtle.turnLeft(), 120)
        self.assertEqual(self.turtle.turnLeft(), 150)
    
    def test_draw(self):
        self.turtle.draw(10)
        for i in range(10, 20):
            self.assertEqual(self.renderer.pixels[i], [255, 255, 255]*10 +  [0, 0, 0] + [255, 255, 255]*9)

    def test_push(self):
        self.turtle.push()
        self.turtle.draw(10)
        self.assertEqual([self.turtle.x, self.turtle.y], [10, 9])

    def test_pop(self):
        self.turtle.push()
        self.turtle.draw(10)
        self.turtle.pop()
        self.assertEqual([self.turtle.x, self.turtle.y], [10, 19])

    def test_reset(self):
        self.turtle.draw(10)
        self.turtle.reset(10, 10, 35)
        self.assertEqual([self.turtle.x, self.turtle.y, self.turtle.angle], [10, 10, 35.0])
        for i in range(10, 20): # the render was not touched, just the turtle
            self.assertEqual(self.renderer.pixels[i], [255, 255, 255]*10 +  [0, 0, 0] + [255, 255, 255]*9)

    def test_setAngle(self):
        self.turtle.setAngle(66)
        self.assertEqual(self.turtle.turn_angle, 66.0)
        self.turtle.setAngle(366)
        self.assertEqual(self.turtle.turn_angle, 6.0)
        self.turtle.setAngle(-6)
        self.assertEqual(self.turtle.turn_angle, 354.0)

    def test_setRandomAngle(self):
        self.turtle.setRandomAngle(66)
        self.assertEqual(self.turtle.random_angle, 66.0)
        self.turtle.setRandomAngle(366)
        self.assertEqual(self.turtle.random_angle, 6.0)
        self.turtle.setRandomAngle(-6)
        self.assertEqual(self.turtle.random_angle, 354.0)

    def test_integration_turtleDrawing(self):
        self.turtle.reset(1,1,0)
        self.turtle.setAngle(90)
        self.turtle.draw(10)
        self.turtle.turnRight()
        self.turtle.draw(10)
        self.turtle.turnRight()
        self.turtle.draw(10)
        self.turtle.turnRight()
        self.turtle.draw(10)
        self.turtle.reset(3,3,315)
        self.turtle.push()
        self.turtle.draw(3.0/math.cos(math.radians(45)))
        self.turtle.pop()
        self.turtle.setAngle(45)
        self.turtle.turnLeft()
        self.turtle.draw(3)
        self.renderer.render('./integration_turtleDrawing.png')

        for i in [2, 7, 8, 9, 10]:
            self.assertEqual(self.renderer.pixels[i], [255, 255, 255] +  [0, 0, 0] + [255, 255, 255]*9 + [0, 0, 0] + [255, 255, 255]*8)

        self.assertEqual(self.renderer.pixels[3], [255, 255, 255] +  [0, 0, 0] + [255, 255, 255] + [0,0,0]*4 + [255, 255, 255]*4 + [0,0,0] + [255, 255, 255]*8)
        self.assertEqual(self.renderer.pixels[4], [255, 255, 255] +  [0, 0, 0] + [255, 255, 255]*2 + [0,0,0] + [255, 255, 255]*6 + [0,0,0] + [255, 255, 255]*8)
        self.assertEqual(self.renderer.pixels[5], [255, 255, 255] +  [0, 0, 0] + [255, 255, 255]*3 + [0,0,0] + [255, 255, 255]*5 + [0,0,0] + [255, 255, 255]*8)
        self.assertEqual(self.renderer.pixels[6], [255, 255, 255] +  [0, 0, 0] + [255, 255, 255]*4 + [0,0,0] + [255, 255, 255]*4 + [0,0,0] + [255, 255, 255]*8)

        self.assertEqual(self.renderer.pixels[1], [255, 255, 255] +  [0, 0, 0]*11 + [255, 255, 255]*8)
        self.assertEqual(self.renderer.pixels[11], [255, 255, 255] +  [0, 0, 0]*11 + [255, 255, 255]*8)
Exemple #3
0
class Raytracer:
    """A class that implements a basic ray tracer. Not intended to provide complex functions, but rather to call them.
    
    :Methods:
        - 'render': Calculates the pixels and outputs them with its own Renderer object
        - 'addSphere': Basic sample function that adds a sphere to the scene
    
    :Examples:
        >>> from pygraph.raytrace.Raytracer import Raytracer
        >>> raytracer.render()
    """

    def __init__(self):
        self.primitives = []
        self.point_lights = []
        self.ambient = [0.0,0.0,0.0]
        self.pixels = []

        self.output_width = 100
        self.output_height = 100
        self.output_file = "default_output.png"

        self.minimum_distance_from_camera = 0.01
        self.renderer = 'NONE'
        
        self.camera_origin = "NONE"
        self.camera_forward = "NONE"
        self.camera_up = "NONE"
        self.camera_right = "NONE"
        self.screen_horizonal = "NONE"
        self.screen_vertical = "NONE"

    def addPrimitive(self, primitive):
        self.primitives.append(primitive)

    def addPointLight(self, origin, color, strength):
        self.point_lights.append([origin.duplicate(), color, strength])

    def setAmbient(self, color):
        self.ambient = list(color)

    def setCamera(self, origin, forward, up, field_of_view):
        self.camera_origin = origin.duplicate()
        self.camera_forward = forward.normalize()
        self.camera_up = up.normalize()
        self.camera_right = forward.cross(up).normalize()

        self.screen_halfwidth = tan(radians(field_of_view/2.0))
        self.screen_halfheight = tan(radians((self.output_height/self.output_width) * field_of_view/2.0))

    def setOutput(self, out_file, out_width, out_height):
        self.output_file = out_file
        self.output_width = out_width
        self.output_height = out_height

        self.renderer = Renderer(self.output_width, self.output_height)
        self.renderer.setBackgroundColor(R=80, G=80, B=80)

    def render(self):
        """Calculates each pixel by shooting rays through them from a camera

        :Explaination:
            For each pixel:
                pixel_color = color from rays collisions
                ray = point on camera plane - camera
                point on camera plane = center of plane + distance right + distance up
                center of plane = camera + forward
                distance right = du * right vector
                distance up = dv * up vector
                du = % along right vector = ((x - pixel_width/2) / pixel_width) * screen_halfwidth
                du factored = (2x - pixel_width) * screen_halfwidth / 2*pixel_width
                We can calculate the part outside the brackets outside of the for loop
                And do a similar thing for height
            So:
                du = (2x - pixel_width) * width_shortcut
                dv = (2y - pixel_height) * height_shortcut
                We need to flip dv to match the fact that y increases as we go down (technical issue, not really geometric)
        """
        width_shortcut = self.screen_halfwidth / (2 * self.output_width)
        height_shortcut = self.screen_halfheight / (2 * self.output_height)
        center_of_camera_plane = self.camera_origin + self.camera_forward

        for y in range(self.output_height):
            for x in range(self.output_width):
                du = (2*x - self.output_width) * width_shortcut
                dv = -(2*y - self.output_height) * height_shortcut

                point_on_camera_plane = center_of_camera_plane + self.camera_right * du + self.camera_up * dv
                ray = (point_on_camera_plane - self.camera_origin).normalize()

                collision, collided_object = self.findClosestCollision(self.camera_origin, ray)
                if (collision != 'NONE' and collision > self.minimum_distance_from_camera):
                    self.renderer.drawOver([[x, y, [int(255*i) for i in self.calculateColor(self.camera_origin, ray, collision, collided_object)]]])

        self.renderer.render(file_name=self.output_file)

    def findClosestCollision(self, origin, ray):
        collision, collision_object = 'NONE', 'NONE'
        for primitive in self.primitives:
            intersect = primitive.intersect(origin, ray)
            if (intersect != "NONE" and intersect < collision):
                collision, collision_object = intersect, primitive
        return [collision, collision_object]

    def calculateColor(self, origin, ray, dist, primitive):
        p_collision = origin + ray * dist
        v_normal = primitive.normalAt(p_collision)

        amb_diff = [primitive.diffuse_color[0] * self.ambient[0], primitive.diffuse_color[1] * self.ambient[1], primitive.diffuse_color[2] * self.ambient[2]]
        color_local = list(amb_diff)

        for light in self.point_lights:
            v_light = (light[0] - p_collision).normalize()
            v_reflected = (v_normal * 2 * v_normal.dot(v_light) - v_light).normalize()
            c_light = [(float(light[2]) * i) for i in light[1]]
            attenuation = 1.0/(light[0] - p_collision).length()

            i_diffuse = max(0.0, v_normal.dot(v_light))
            i_specular = max(0.0, v_reflected.dot(v_light)) ** primitive.shininess

            c_diffuse = [primitive.diffuse_constant * i_diffuse * i for i in primitive.diffuse_color]
            c_specular = [primitive.diffuse_constant * i_specular * i for i in primitive.specular_color]

            spec_diff = [attenuation * (i[0] + i[1]) for i in zip(c_diffuse, c_specular)]
            spec_diff = [i[0] * i[1] for i in zip(c_light, spec_diff)] # Light color * Material Color(with shading)

            color_local = [i[0] + i[1] for i in zip(color_local, spec_diff)] # add the color created by this light to the current color

        # Here we could recursively fire another ray, if we add a recursion number to calculateColor
        color_reflected = [0.0, 0.0, 0.0]
        color_refracted = [0.0, 0.0, 0.0]

        color = color_local # + color_reflected + color_refracted

        # We might have a value greater than 1 for a component. We need to divide by the maximum to lower intensity whilst retaining the color
        max_color = max(color[0], color[1], color[2])
        if (max_color > 1.0):
            color = [i/max_color for i in color]
        return color