“Ray Tracing is the Future and Ever Will Be”
英伟达的一位大佬曾经这么说过:「光线追踪是未来,而且将永远是未来。」
光线追踪的原理是非常直观而优雅的,其应用的核心难点在于性能优化。本文将基于光线追踪的基础原理,用 100 行 python 代码实现一个简单的光线追踪渲染器。
基础原理
我们看到的物体,是由来自各个方向发射光、反射光、折射光照亮的。而光线的起点,是各种发光源;光线的终点,则是我们的眼睛。一束光由光源出发,经过不同物体的反射、折射,最终射入观察者的眼睛。
由于在几何光学中,光路具有可逆性。我们从观察者的眼睛射出一道虚拟的光线,它经过的路径将与射入观察者眼睛的那道光路径完全一致,方向相反。此时,如果我们在观察者眼前放置一个像素化的「窗口」,由观察者向每个像素发射一道虚拟光线,最终,这些光线都会「返回」到光源中。结合这些光的路径,和相关的物理模型/经验模型,我们可以计算出这个窗口上每一个像素点的颜色值,从而形成我们在计算机屏幕上看到的图像。
场景布置
为了简化代码,我们尽可能将场景简单化,并使用参数方程描述场景中的物体。
scene = [sphere([.75, .1, 1.], .6, [.8, .3, 0.]), # 球心位置,半径,颜色
sphere([-.3, .01, .2], .3, [.0, .0, .9]),
sphere([-2.75, .1, 3.5], .6, [.1, .572, .184]),
plane([0., -.5, 0.], [0., 1., 0.])] # 平面上一点的位置,法向量
light_point = np.array([5., 5., -10.]) # 点光源位置
light_color = np.array([1., 1., 1.]) # 点光源的颜色值
ambient = 0.05 # 环境光
我们在场景里放置三个球和一个平面,并放置了一个白色点光源。
在现实世界中,完全黑暗不可见的场景是很少的。即使是在一个暗室中,点亮一枚微弱的蜡烛,在家具等物品的阴影内,也并不是完全黑暗。这些地方是由光源的光经过多次反射后,近似均匀地投射到各个角落的。为了描述这个复杂的物理现象,我们将其简化为一个较小的常数光照——环境光。现阶段的计算机图形学远无法精确地模拟真实的物理世界,在许多时候(特别是对计算实时性要求较高的时候),我们通常会用一个可接受的简化经验模型来替代相对真实的模型。
生成物品
def get_color(obj, P):
color = obj['color']
if not hasattr(color, '__len__'):
color = color(P)
return color
def sphere(position, radius, color, reflection=.85, diffuse=1., specular_c=.6, specular_k=50):
return dict(type='sphere', position=np.array(position), radius=np.array(radius),
color=np.array(color), reflection=reflection, diffuse=diffuse, specular_c=specular_c, specular_k=specular_k)
def plane(position, normal, color=np.array([1.,1.,1.]), reflection=0.15, diffuse=.75, specular_c=.3, specular_k=50):
return dict(type='plane', position=np.array(position), normal=np.array(normal),
color=lambda P: (np.array([1.,1.,1.]) if (int(P[0]*2)%2) == (int(P[2]*2)%2) else (np.array([0.,0.,0.]))),
reflection=reflection, diffuse=diffuse, specular_c=specular_c, specular_k=specular_k)
我们用 python 的字典对象来描述球和平面的各种参数:位置、半径、法向量、颜色、镜面反射率、漫反射率、高光参数,等。其中一些参数的意义在后面的小节中再进行解释。
其中,我们用了一个匿名函数为平面生成黑白相间的棋盘格纹理。
基础准备
import numpy as np
def normalize(x):
return x / np.linalg.norm(x)
def get_normal(obj, point): # 获得物体表面某点处的单位法向量
if obj['type'] == 'sphere':
return normalize(point - obj['position'])
if obj['type'] == 'plane':
return obj['normal']
我们定义两个简单的函数 normalize 和 get_normal,分别用于将向量归一化,获取物体表面特定点的单位法向量。球面和平面的法向量获取方式相当简单。此处我们引用了 numpy 这个科学计算的库,对此不熟悉的读者可以参考这里进行安装:NumPy 安装教程。
简单的几何学
def intersect(origin, dir, obj): # 射线与物体的相交测试
if obj['type'] == 'plane':
ret