├── README.md ├── fast.py ├── result.png └── tiny.py /README.md: -------------------------------------------------------------------------------- 1 | # Fast and tiny: raytracing in python 2 | 3 | ![](https://raw.githubusercontent.com/ssloy/fast-and-tiny/main/result.png) 4 | 5 | Who said that the code in this repository is fast **and** tiny? :) 6 | 7 | Here are two versions: [tiny.py](https://github.com/ssloy/fast-and-tiny/blob/main/tiny.py) in 62 lines of code, very easy to read, but pretty slow. The other one, [fast.py](https://github.com/ssloy/fast-and-tiny/blob/main/fast.py) is a bit harder to read, but it is orders of magnitude faster. 8 | 9 | # tiny.py: how it works (writing in progress) 10 | 11 | ``` 12 | for each pixel 13 | compute color 14 | ``` 15 | 16 | ![](https://raw.githubusercontent.com/ssloy/fast-and-tiny/46542e64039381dee9a693781fa314d58de873b1/result.png) 17 | 18 | ``` 19 | for each pixel 20 | emit a ray from the origin trough the pixel 21 | if the ray intersects the sphere 22 | paint sphere color 23 | else 24 | paint background 25 | ``` 26 | 27 | ![](https://raw.githubusercontent.com/ssloy/fast-and-tiny/97ee4ca6cc9279bd21b4d0d392c2c31e58da90b7/result.png) 28 | 29 | ``` 30 | for each pixel 31 | emit a ray from the origin trough the pixel 32 | for all objects in the scene 33 | select the frontmost intersection point 34 | if the ray intersects the scene 35 | paint intersection point color 36 | else 37 | paint background 38 | ``` 39 | 40 | ![](https://raw.githubusercontent.com/ssloy/fast-and-tiny/3a3571f4c829576d4c256125064033bf8aef6c6a/result.png) 41 | 42 | ![](https://raw.githubusercontent.com/ssloy/fast-and-tiny/09f957b4f4112f5a2e502e6f1bfa445ad73aa83c/result.png) 43 | 44 | ``` 45 | for each pixel 46 | emit a ray from the origin trough the pixel 47 | for all objects in the scene 48 | select the frontmost intersection point 49 | if the ray intersects the scene 50 | recursively emit a reflected ray 51 | cumulate colors through reflexions 52 | else 53 | paint background 54 | ``` 55 | 56 | 57 | ![](https://raw.githubusercontent.com/ssloy/fast-and-tiny/37088b33a3e1ce68d86e4e6097408a2d9b3b0838/result.png) 58 | 59 | ![](https://raw.githubusercontent.com/ssloy/fast-and-tiny/721b67309ab24998d870fcd0a3a8e8c04edfb680/result.png) 60 | 61 | # fast.py: how it works (writing in progress) 62 | -------------------------------------------------------------------------------- /fast.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | 4 | def box_intersect(bmin, bmax, ray_origins, ray_directions): 5 | ray_directions = np.where(np.abs(ray_directions)<1e-3, 1e-3, ray_directions) 6 | sign_mask = np.sign(ray_directions) 7 | entries = (np.where(sign_mask == 1, bmin, bmax) - ray_origins) / ray_directions 8 | t, t_axes = np.max(entries, axis=1), np.argmax(entries, axis=1) 9 | points = ray_origins + t[:, np.newaxis]*ray_directions 10 | hit = (t>0) & np.all((points>bmin-1e-3) & (points0] - np.sqrt(delta[delta>0]) 19 | hit = np.zeros(ray_origins.shape[0], dtype=bool) 20 | hit[delta>0] = t>0 21 | points = ray_origins[hit] + t[t>0, np.newaxis]*ray_directions[hit] 22 | return hit, points, (points - center) / radius 23 | 24 | def scene_intersect(ray_origins, ray_directions): # find closest point in the scene along the ray 25 | nrays = ray_origins.shape[0] 26 | nearest = np.full(nrays, np.inf) # the (squared) distance from the ray origin to the nearest point in the scene 27 | points = np.zeros_like(ray_origins) # nearest point, \ 28 | normals = np.zeros_like(ray_origins) # its normal, | the information about the intersection points we want to return 29 | colors = np.zeros_like(ray_origins) # color of the surface, | (junk values if no intersection) 30 | hot = np.full(nrays, False) # hot or not / 31 | for o in [ {'center': np.array([ 6, 0, 7]), 'radius': 2, 'color': np.array([1., .4, .6]), 'hot': False}, # description of the scene: 32 | {'center': np.array([2.8, 1.1, 7]), 'radius': .9, 'color': np.array([1., 1., .3]), 'hot': False}, # three spheres and two boxes 33 | {'center': np.array([ 5, -10, -7]), 'radius': 8, 'color': np.array([1., 1., 1.]), 'hot': True}, # one of the spheres is "hot" (incandescent) 34 | {'min': np.array([3, -4, 11]), 'max': np.array([ 7, 2, 13]), 'color': np.array([.4, .7, 1.]), 'hot': False}, 35 | {'min': np.array([0, 2, 6]), 'max': np.array([11, 2.2, 16]), 'color': np.array([.6, .7, .6]), 'hot': False} ]: 36 | if 'center' in o: # is it a sphere or a box? 37 | hit,p,n = sphere_intersect(o['center'], o['radius'], ray_origins, ray_directions) 38 | else: 39 | hit,p,n = box_intersect(o['min'], o['max'], ray_origins, ray_directions) 40 | z = (p-ray_origins[hit]) 41 | dist = np.sum(z**2, axis=1) 42 | closer = distmaxdepth: return np.full(ray_origins.shape, ambient_color) 59 | hit,points,normals,colors,hot = scene_intersect(ray_origins, ray_directions) 60 | colors[~hit] = ambient_color 61 | bounce = hit & ~hot 62 | colors[bounce] = colors[bounce] * trace(points[bounce], reflect(ray_directions[bounce], normals[bounce]), depth+1) 63 | return colors 64 | 65 | width, height, ambient_color = 640, 480, np.array([.5]*3) 66 | focal, azimuth = 500, 30*np.pi/180 67 | nrays, maxdepth = 10, 3 68 | 69 | x, z = np.tile(np.linspace(-width/2, width/2, width), height), np.full(width*height, focal) 70 | eye = np.zeros((width*height,3)) 71 | rays = normalized(np.column_stack(( 72 | np.cos(azimuth)*x + np.sin(azimuth)*z, # dir x 73 | np.repeat(np.linspace(-height/2, height/2, height), width), # dir y 74 | -np.sin(azimuth)*x + np.cos(azimuth)*z))) # dir z 75 | image = np.zeros((height*width, 3)) 76 | for r in range(nrays): 77 | print("Pass %d/%d" % (r + 1, nrays)) 78 | image += trace(eye, rays, 0) 79 | plt.imsave('result.png', np.clip(image.reshape(height, width, 3)/nrays, 0, 1)) 80 | -------------------------------------------------------------------------------- /result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssloy/fast-and-tiny/e226704d01a60074386019072885021350228016/result.png -------------------------------------------------------------------------------- /tiny.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | def box_intersect(bmin, bmax, ray_origin, ray_direction): 5 | ray_direction = np.where(np.abs(ray_direction)<1e-3, 1e-3, ray_direction) # avoid division by zero 6 | entries = (np.where(np.sign(ray_direction) == 1, bmin, bmax) - ray_origin) / ray_direction # here we test against 3 planes (instead of 6), i.e. 7 | t, t_axis = np.max(entries), np.argmax(entries) # no rendering from the inside of a box 8 | point = ray_origin + t * ray_direction # intersection between the ray and the plane 9 | normal = np.zeros(3) # normal at the intersection 10 | normal[t_axis] = -np.sign(ray_direction[t_axis]) # both point and normal contain junk values if no intersection 11 | return (t>0) and np.all((point>bmin-1e-3) & (point0 and (t:=proj - np.sqrt(delta)) > 0: # the smallest root suffices (one-sided walls, no rendering from the inside of a sphere) 17 | point = ray_origin + t * ray_direction 18 | return True,point,(point-center)/radius # we have a hit, intersection point, surface normal at the point 19 | return False,None,None # no intersection 20 | 21 | def scene_intersect(ray_origin, ray_direction): 22 | nearest = np.inf # the (squared) distance from the ray origin to the nearest point in the scene 23 | point,normal,color,hot = None,None,None,None # the information about the intersection point we want to return 24 | for o in [ {'center': np.array([ 6, 0, 7]), 'radius': 2, 'color': np.array([1., .4, .6]), 'hot': False}, # description of the scene: 25 | {'center': np.array([2.8, 1.1, 7]), 'radius': .9, 'color': np.array([1., 1., .3]), 'hot': False}, # three spheres and two boxes 26 | {'center': np.array([ 5, -10, -7]), 'radius': 8, 'color': np.array([1., 1., 1.]), 'hot': True}, # one of the spheres is "hot" (incandescent) 27 | {'min': np.array([3, -4, 11]), 'max': np.array([ 7, 2, 13]), 'color': np.array([.4, .7, 1.]), 'hot': False}, 28 | {'min': np.array([0, 2, 6]), 'max': np.array([11, 2.2, 16]), 'color': np.array([.6, .7, .6]), 'hot': False} ]: 29 | if 'center' in o: # is it a sphere or a box? 30 | hit,p,n = sphere_intersect(o['center'], o['radius'], ray_origin, ray_direction) 31 | else: 32 | hit,p,n = box_intersect(o['min'], o['max'], ray_origin, ray_direction) 33 | if hit and (d2:=np.dot(p-ray_origin, p-ray_origin)) ambient color 49 | 50 | width, height, ambient_color = 640, 480, np.array([.5]*3) 51 | focal, azimuth = 500, 30*np.pi/180 52 | nrays, maxdepth = 10, 3 53 | image = np.zeros((height, width, 3)) 54 | for i in range(height): 55 | for j in range(width): 56 | ray = normalized(np.array([j-width/2, i-height/2, focal])) # emit the ray along Z axis 57 | ray[0],ray[2] = (np.cos(azimuth)*ray[0] + np.sin(azimuth)*ray[2], # and then rotate it 30 degrees around Y axis 58 | -np.sin(azimuth)*ray[0] + np.cos(azimuth)*ray[2]) 59 | for r in range(nrays): 60 | image[i, j] += trace(np.zeros(3), ray, 0) 61 | print("%d/%d" % (i + 1, height)) 62 | plt.imsave('result.png', np.clip(image/nrays, 0, 1)) 63 | --------------------------------------------------------------------------------