martinchristen/pyRT

View on GitHub
pyrt/renderer/simplert.py

Summary

Maintainability
A
1 hr
Test Coverage
"""
A simple ray tracer

Reference implementation...
"""

from .rgbimage import *
from .renderer import Renderer
from ..scene import Scene
from ..math import *

import time


class SimpleRT(Renderer):
    def __init__(self, shadow=False, iterations=1):
        Renderer.__init__(self, "Simple Raytracer")
        self.shadow = shadow
        self.iterations = iterations

        if self.shadow:
            print("# Shadow Enabled")
        if self.iterations>1:
            print("# Iterations: " + str(self.iterations))

    def _shade(self, scene: Scene, ray: Ray, hitrecord: HitRecord) -> tuple:
        r = g = b = 0  # background color
        hit = False
        for element in scene.nodes:
            if element.hit(ray, hitrecord):
                hit = True
                hitrecord.obj = element  # set hit object

                # element hit -> call shader:
                color = element.material.shade(scene.camera, ray, hitrecord, scene.lights)
                r = int(color[0] * 255)
                g = int(color[1] * 255)
                b = int(color[2] * 255)

                # Calculate shadow
                if self.shadow:
                    # is hitpoint in shadow ?
                    f, n = self._shadow(scene, hitrecord)
                    self.num_shadow_rays += n
                    r = int(r * f)
                    g = int(g * f)
                    b = int(b * f)


        return hit, r, g, b

    def _shadow(self, scene: Scene, hitrecord: HitRecord) -> tuple:
        # is hitpoint in shadow ?
        fs = 0.0
        local_num_shadow_rays = 0
        numshadow = 0
        for light in scene.lights:
            shadowray = Ray(hitrecord.point, light.position - hitrecord.point)
            fs += light.intensity(shadowray) 

            local_num_shadow_rays += 1
            for testelement in scene.nodes:
                if testelement != hitrecord.obj:  # avoid self-intersection
                    if testelement.hitShadow(shadowray):
                        numshadow += 1
                        break

        fs = np.clip(fs, 0, 1)
        for i in range(numshadow):
            fs /= 4.

        return fs,local_num_shadow_rays
    
    def _recurse_shade(self, scene: Scene, ray: Ray, iteration_num: int) -> tuple:
        hitrecord = HitRecord()
        hit, r, g, b = self._shade(scene, ray, hitrecord)

        if not hit or iteration_num == 0:
            return r, g, b
        
        if abs(hitrecord.material.reflectivity) > G_EPSILON:
            reflect_ray = Ray(hitrecord.point, reflect3(hitrecord.normal_g, ray.direction))
            new_r, new_g, new_b = self._recurse_shade(scene, reflect_ray, iteration_num - 1)

            ref1 = hitrecord.material.reflectivity
            ref2 = 1.-ref1
            r = int ( ref1 * new_r + ref2 * r)
            g = int ( ref1 * new_g + ref2 * g)
            b = int ( ref1 * new_b + ref2 * b)

            self.num_secondary_rays += 1

        if abs(hitrecord.material.transparency) > G_EPSILON:
            refract_ray = Ray(
                hitrecord.point + ray.direction * G_REFRACTION_EPSILON, 
                refract3(normalize3(hitrecord.normal_g), normalize3(ray.direction), hitrecord.material.refraction)
            )
            new_r, new_g, new_b = self._recurse_shade(scene, refract_ray, iteration_num - 1)

            ref1 = hitrecord.material.transparency
            ref2 = 1.-ref1
            r = int ( ref1 * new_r + ref2 * r)
            g = int ( ref1 * new_g + ref2 * g)
            b = int ( ref1 * new_b + ref2 * b)

            self.num_secondary_rays += 1
        
        return r, g, b

    def render(self, scene: Scene) -> RGBImage:
        if not scene.camera:
            print("Warning: Can't render: there is no (active) camera in the scene!")
            return None

        time_start = time.time()
        self.num_rays = 0
        self.num_shadow_rays  = 0
        self.num_secondary_rays = 0
        hitrecord = HitRecord()

        w = scene.camera.width
        h = scene.camera.height

        image = RGBImage(w,h)

        for y in range(0, h):
            for x in range(0, w):
                ray = scene.camera.primaryRay(x, y)
                hitrecord.reset()
                self.num_rays += 1

                # Primary Ray:
                r, g, b = self._recurse_shade(scene, ray, self.iterations - 1)
                image.drawPixelFast8(x, y, r, g, b)


        time_end = time.time()
        print("# RENDER STATISTICS" + 31 * "#")
        time_total = time_end - time_start
        print("TIME FOR RENDERING: " + str(time_total) + "s")
        print("NUMBER OF PRIMARY RAYS: " + str(self.num_rays))
        print("NUMBER OF SECONDARY RAYS: " + str(self.num_secondary_rays))
        print("NUMBER OF SHADOW RAYS: " + str(self.num_shadow_rays))
        print("RAYS/s: " + str((self.num_rays + self.num_secondary_rays + self.num_shadow_rays) / time_total))
        print(50 * "#")

        return image