Ray tracer à partir de zéro en 100 lignes de Python

Image 1
Image 1

Dans cet article, nous allons jeter un coup d'œil sous le capot des algorithmes d'infographie, parcourir les principes de base du lancer de rayons étape par étape et écrire une implémentation simple en Python. Pas de bibliothèques graphiques tierces - juste NumPy et du code nu dans le compilateur.





: / , , .





:





  • A B — : 1, 2, 3,…, n, — , A B, B — A ();





  • — — , . v ||v||;





  • — 1: ||v|| = 1;





  • , , 1, — : u = v/||v||;





  • : <v, v> = ||v||².





, . .





, :





  • ( );





  • ( . 1, );





  • ( , );





  • ;





  • , ( ).





Figure 2
2

— (, 3x2). 3 2 - . , (), . . , 3x2 300x200 .





:





p(x, y, z) :





    p





    (), p, , :





       





        , :





           





            p





figure 3
3

, . , . , , , , ( ).





, , . , . , .





Figure 4
4

, (x = 0, y = 0, z = 1), , x y. .





import numpy as np
import matplotlib.pyplot as plt
width = 300
height = 200
camera = np.array([0, 0, 1])
ratio = float(width) / height
screen = (-1, 1 / ratio, 1, -1 / ratio) # , , , 
image = np.zeros((height, width, 3))
for i, y in enumerate(np.linspace(screen[1], screen[3], height)):
    for j, x in enumerate(np.linspace(screen[0], screen[2], width)):
        # image[i, j] = ...
        print("progress: %d/%d" % (i + 1, height))
plt.imsave('image.png', image)
      
      



  • — , ;





  • , ( ): , , , . -1 1 x -1/ratio 1/ratio y, ratio — , . , , , . ( ): 2 /(2/ratio) = ratio, 300x200;





  • x y, ;





  • — — . 





: (), p, , ...





. , () p?





«», «». , : , , .





, , , . :





, — 3D-. t = 0 , t . , t.





, , (O) (D) :





d .





:





import numpy as np
import matplotlib.pyplot as plt
def normalize(vector):
    return vector / np.linalg.norm(vector)
width = 300
height = 200
camera = np.array([0, 0, 1])
ratio = float(width) / height
screen = (-1, 1 / ratio, 1, -1 / ratio) # , , , 
image = np.zeros((height, width, 3))
for i, y in enumerate(np.linspace(screen[1], screen[3], height)):
    for j, x in enumerate(np.linspace(screen[0], screen[2], width)):
        pixel = np.array([x, y, 0])
        origin = camera
        direction = normalize(pixel - origin)
        # image[i, j] = ...
    print("progress: %d/%d" % (i + 1, height))
plt.imsave('image.png', image)
      
      



  • normalize(vector), ... , ;





  • , . , z = 0, , , x y;





, . .





— . , r () ().





, C r X , :





, , X — C:





:





objects = [
   { 'center': np.array([-0.2, 0, -1]), 'radius': 0.7 },
   { 'center': np.array([0.1, -0.3, 0]), 'radius': 0.1 },
   { 'center': np.array([-0.3, 0, 0]), 'radius': 0.15 }
]
      
      



.





, , . , , t. , : t ray(t) ?





, t. , t², t¹, t⁰, a, b c, . :





d , a = 1. :





Figure 5
5

. , . , , None:





def sphere_intersect(center, radius, ray_origin, ray_direction):
   b = 2 * np.dot(ray_direction, ray_origin — center)
   c = np.linalg.norm(ray_origin — center) ** 2 — radius ** 2
   delta = b ** 24 * c
   if delta > 0:
       t1 = (-b + np.sqrt(delta)) / 2
       t2 = (-b — np.sqrt(delta)) / 2
       if t1 > 0 and t2 > 0:
           return min(t1, t2)
   return None
      
      



, , t1 t2 . , , , , d , -d (, ).





: (), p, , [...]. .





, sphere_intersect() , , . , :





def nearest_intersected_object(objects, ray_origin, ray_direction):
   distances = [sphere_intersect(obj['center'], obj['radius'], ray_origin, ray_direction) for obj in objects]
   nearest_object = None
   min_distance = np.inf
   for index, distance in enumerate(distances):
       if distance and distance < min_distance:
           min_distance = distance
           nearest_object = objects[index]
   return nearest_object, min_distance
      
      



, nearest_object = None, , , min_distance — .





, :





nearest_object, distance = nearest_intersected_object(objects, o, d)
if nearest_object:
   intersection_point = o + d * distance
      
      



:





import numpy as np
import matplotlib.pyplot as plt
def normalize(vector):
   return vector / np.linalg.norm(vector)
def sphere_intersect(center, radius, ray_origin, ray_direction):
   b = 2 * np.dot(ray_direction, ray_origin — center)
   c = np.linalg.norm(ray_origin — center) ** 2 — radius ** 2
   delta = b ** 24 * c
   if delta > 0:
       t1 = (-b + np.sqrt(delta)) / 2
       t2 = (-b — np.sqrt(delta)) / 2
       if t1 > 0 and t2 > 0:
           return min(t1, t2)
   return None
def nearest_intersected_object(objects, ray_origin, ray_direction):
   distances = [sphere_intersect(obj['center'], obj['radius'], ray_origin, ray_direction) for obj in objects]
   nearest_object = None
   min_distance = np.inf
   for index, distance in enumerate(distances):
       if distance and distance < min_distance:
           min_distance = distance
           nearest_object = objects[index]
   return nearest_object, min_distance
width = 300
height = 200
camera = np.array([0, 0, 1])
ratio = float(width) / height
screen = (-1, 1 / ratio, 1, -1 / ratio) # , , , 
objects = [
   { 'center': np.array([-0.2, 0, -1]), 'radius': 0.7 },
   { 'center': np.array([0.1, -0.3, 0]), 'radius': 0.1 },
   { 'center': np.array([-0.3, 0, 0]), 'radius': 0.15 }
]
image = np.zeros((height, width, 3))
for i, y in enumerate(np.linspace(screen[1], screen[3], height)):
   for j, x in enumerate(np.linspace(screen[0], screen[2], width)):
       pixel = np.array([x, y, 0])
       origin = camera
       direction = normalize(pixel — origin)
       #  
       nearest_object, min_distance = nearest_intersected_object(objects, origin, direction)
       if nearest_object is None:
           continue
       #       
       intersection = origin + min_distance * direction
       # image[i, j] = ...
       print("%d/%d" % (i + 1, height))
plt.imsave('image.png', image)
      
      



, , , , , . , . , , , . , — , .





, : near_intersected_object(). , , , , . , : . -, . :





light = { 'position': np.array([5, 5, 5]) }
      
      



, , , , , , , ( , ).





# ...
intersection = origin + min_distance * direction
intersection_to_light = normalize(light['position'] — intersection)
_, min_distance = nearest_intersected_object(objects, intersection, intersection_to_light)
intersection_to_light_distance = np.linalg.norm(light['position'] — intersection)
is_shadowed = min_distance < intersection_to_light_distance
      
      



, . . , , , . — , . .





Graphique 6
6

, .





, :





# ...
intersection = origin + min_distance * direction
normal_to_surface = normalize(intersection — nearest_object['center'])
shifted_point = intersection + 1e-5 * normal_to_surface
intersection_to_light = normalize(light['position'] — shifted_point)
_, min_distance = nearest_intersected_object(objects, shifted_point, intersection_to_light)
intersection_to_light_distance = np.linalg.norm(light['position'] — intersection)
is_shadowed = min_distance < intersection_to_light_distance
if is_shadowed:
   continue
      
      



-

, , , — . : ? -.





-, .





, :





  • (Ambient color): , ;





  • (Diffuse color): , , ;





  • (Specular color): , . ;





  • (Shininess): , , .





: RGB 0 – 1.





Graphique 7
7

:





objects = [
   { 'center': np.array([-0.2, 0, -1]), 'radius': 0.7, 'ambient': np.array([0.1, 0, 0]), 'diffuse': np.array([0.7, 0, 0]), 'specular': np.array([1, 1, 1]), 'shininess': 100 },
   { 'center': np.array([0.1, -0.3, 0]), 'radius': 0.1, 'ambient': np.array([0.1, 0, 0.1]), 'diffuse': np.array([0.7, 0, 0.7]), 'specular': np.array([1, 1, 1]), 'shininess': 100 },
   { 'center': np.array([-0.3, 0, 0]), 'radius': 0.15, 'ambient': np.array([0, 0.1, 0]), 'diffuse': np.array([0, 0.6, 0]), 'specular': np.array([1, 1, 1]), 'shininess': 100 }
]
      
      



, , .





- , : , . :





light = { 'position': np.array([5, 5, 5]), 'ambient': np.array([1, 1, 1]), 'diffuse': np.array([1, 1, 1]), 'specular': np.array([1, 1, 1]) }
      
      



, - :









  • ka, kd, ks — , , ;





  • ia, id, is — , , ;





  • L — ;





  • N — ;





  • V — ;





  • α — .





# ...
if is_shadowed:
   break

# RGB
illumination = np.zeros((3))

# ambiant
illumination += nearest_object['ambient'] * light['ambient']

# diffuse
illumination += nearest_object['diffuse'] * light['diffuse'] * np.dot(intersection_to_light, normal_to_surface)

# specular
intersection_to_camera = normalize(camera — intersection)
H = normalize(intersection_to_light + intersection_to_camera)
illumination += nearest_object['specular'] * light['specular'] * np.dot(normal_to_surface, H) ** (nearest_object['shininess'] / 4)
image[i, j] = np.clip(illumination, 0, 1)
      
      



, 0 1, , .





( ).





Figure 8
8

, , :





  • ;





  • .





— , , . , ( ), , .





:





{ 'center': np.array([0, -9000, 0]), 'radius': 90000.7, 'ambient': np.array([0.1, 0.1, 0.1]), 'diffuse': np.array([0.6, 0.6, 0.6]), 'specular': np.array([1, 1, 1]), 'shininess': 100 }
      
      



, , . , , ? .





0 1. 0 , , 1 — . :





{ 'center': np.array([-0.2, 0, -1]), ..., 'reflection': 0.5 }
{ 'center': np.array([0.1, -0.3, 0]), ..., 'reflection': 0.5 }
{ 'center': np.array([-0.3, 0, 0]), ..., 'reflection': 0.5 }
{ 'center': np.array([0, -9000, 0]), ..., 'reflection': 0.5 }
      
      



, , , . .





Graphique 9
9

, :









  • c — ;





  • i — , - ;





  • r — .





( , ), .





, . :









  • R — ;





  • L — ;





  • N — .





Graphique 10
10

normalize():





def reflected(vector, axis):
   return vector — 2 * np.dot(vector, axis) * axis
      
      



#  
max_depth = 3

#    

color = np.zeros((3))
reflection = 1

for k in range(max_depth):
   nearest_object, min_distance = # ...

   # ...
   illumination += # ...

   # 
   color += reflection * illumination
   reflection *= nearest_object['reflection']

   #      
   origin = shifted_point
   direction = reflected(direction, normal_to_surface)
image[i, j] = np.clip(color, 0, 1)
      
      



: , , break .





. :





Graphique 11
11

:





import numpy as np
import matplotlib.pyplot as plt

def normalize(vector):
    return vector / np.linalg.norm(vector)

def reflected(vector, axis):
    return vector - 2 * np.dot(vector, axis) * axis


def sphere_intersect(center, radius, ray_origin, ray_direction):
    b = 2 * np.dot(ray_direction, ray_origin - center)
    c = np.linalg.norm(ray_origin - center) ** 2 - radius ** 2
    delta = b ** 2 - 4 * c
    if delta > 0:
        t1 = (-b + np.sqrt(delta)) / 2
        t2 = (-b - np.sqrt(delta)) / 2
        if t1 > 0 and t2 > 0:
            return min(t1, t2)
    return None

def nearest_intersected_object(objects, ray_origin, ray_direction):
    distances = [sphere_intersect(obj['center'], obj['radius'], ray_origin, ray_direction) for obj in objects]
    nearest_object = None
    min_distance = np.inf
    for index, distance in enumerate(distances):
        if distance and distance < min_distance:
            min_distance = distance
            nearest_object = objects[index]
    return nearest_object, min_distance

width = 300
height = 200
max_depth = 3

camera = np.array([0, 0, 1])
ratio = float(width) / height
screen = (-1, 1 / ratio, 1, -1 / ratio) # left, top, right, bottom

light = { 'position': np.array([5, 5, 5]), 'ambient': np.array([1, 1, 1]), 'diffuse': np.array([1, 1, 1]), 'specular': np.array([1, 1, 1]) }

objects = [
    { 'center': np.array([-0.2, 0, -1]), 'radius': 0.7, 'ambient': np.array([0.1, 0, 0]), 'diffuse': np.array([0.7, 0, 0]), 'specular': np.array([1, 1, 1]), 'shininess': 100, 'reflection': 0.5 },
    { 'center': np.array([0.1, -0.3, 0]), 'radius': 0.1, 'ambient': np.array([0.1, 0, 0.1]), 'diffuse': np.array([0.7, 0, 0.7]), 'specular': np.array([1, 1, 1]), 'shininess': 100, 'reflection': 0.5 },
    { 'center': np.array([-0.3, 0, 0]), 'radius': 0.15, 'ambient': np.array([0, 0.1, 0]), 'diffuse': np.array([0, 0.6, 0]), 'specular': np.array([1, 1, 1]), 'shininess': 100, 'reflection': 0.5 },
    { 'center': np.array([0, -9000, 0]), 'radius': 9000 - 0.7, 'ambient': np.array([0.1, 0.1, 0.1]), 'diffuse': np.array([0.6, 0.6, 0.6]), 'specular': np.array([1, 1, 1]), 'shininess': 100, 'reflection': 0.5 }
]

image = np.zeros((height, width, 3))
for i, y in enumerate(np.linspace(screen[1], screen[3], height)):
    for j, x in enumerate(np.linspace(screen[0], screen[2], width)):

        #    

        pixel = np.array([x, y, 0])
        origin = camera
        direction = normalize(pixel - origin)

        color = np.zeros((3))
        reflection = 1

        for k in range(max_depth):

            #  

            nearest_object, min_distance = nearest_intersected_object(objects, origin, direction)
            if nearest_object is None:

                break

            intersection = origin + min_distance * direction
            normal_to_surface = normalize(intersection - nearest_object['center'])
            shifted_point = intersection + 1e-5 * normal_to_surface
            intersection_to_light = normalize(light['position'] - shifted_point)

            _, min_distance = nearest_intersected_object(objects, shifted_point, intersection_to_light)

            intersection_to_light_distance = np.linalg.norm(light['position'] - intersection)
            is_shadowed = min_distance < intersection_to_light_distance

            if is_shadowed:
                break

            illumination = np.zeros((3))

            # ambiant

            illumination += nearest_object['ambient'] * light['ambient']

            # diffuse

            illumination += nearest_object['diffuse'] * light['diffuse'] * np.dot(intersection_to_light, normal_to_surface)

            # specular

            intersection_to_camera = normalize(camera - intersection)
            H = normalize(intersection_to_light + intersection_to_camera)
            illumination += nearest_object['specular'] * light['specular'] * np.dot(normal_to_surface, H) ** (nearest_object['shininess'] / 4)

            # reflection

            color += reflection * illumination
            reflection *= nearest_object['reflection']
            origin = shifted_point
            direction = reflected(direction, normal_to_surface)
        image[i, j] = np.clip(color, 0, 1)
    print("%d/%d" % (i + 1, height))
plt.imsave('image.png', image)
      
      



?

, . . :





  • , , , , ;





  • . POO , ;





  • , (, ) ;





  • ;





  • -. , «» . «» , 2D- 3D-: .





. :





Kotlin ( , Python) GitHub.








All Articles