第九章:数字
处理数字是 Common Lisp 的强项之一。Common Lisp 有着丰富的数值类型,而 Common Lisp 操作数字的特性与其他语言比起来更受人喜爱。
9.1 类型 (Types)
Common Lisp 提供了四种不同类型的数字:整数、浮点数、比值与复数。本章所讲述的函数适用于所有类型的数字。有几个不能用在复数的函数会特别说明。
整数写成一串数字:如 2001
。浮点数是可以写成一串包含小数点的数字,如 253.72
,或是用科学表示法,如 2.5372e2
。比值是写成由整数组成的分数:如 2/3
。而复数 a+bi
写成 #c(a b)
,其中 a
与 b
是任两个类型相同的实数。
谓词 integerp
、 floatp
以及 complexp
针对相应的数字类型返回真。图 9.1 展示了数值类型的层级。
朗伯定律 告诉我们,由平面上一点所反射的光的强度,正比于该点的单位法向量 (unit normal vector) N (这里是与平面垂直且长度为一的向量)与该点至光源的单位向量 L 的点积 (dot-product):
i=N⋅L
如果光刚好照到这点, N 与 L 会重合 (coincident),则点积会是最大值, 1
。如果将在这时候将平面朝光转 90 度,则 N 与 L 会垂直,则两者点积会是 0
。如果光在平面后面,则点积会是负数。
在我们的程序里,我们假设光源在观测点 (eye),所以 lambert
使用了这个规则来找到平面上某点的亮度 (illumination),返回我们追踪的光的单位向量与法向量的点积。
在 sendray
这个值会乘上平面的颜色 (即便是有好的照明,一个暗的平面还是暗的)来决定该点之后总体亮度。
为了简单起见,我们在模拟世界里会只有一种物体,球体。图 9.5 包含了与球体有关的代码。球体结构包含了 surface
,所以一个球体会有一种颜色以及 center
和 radius
。调用 defsphere
添加一个新球体至世界里。
(defstruct (sphere (:include surface))
radius center)
(defun defsphere (x y z r c)
(let ((s (make-sphere
:radius r
:center (make-point :x x :y y :z z)
:color c)))
(push s *world*)
s))
(defun intersect (s pt xr yr zr)
(funcall (typecase s (sphere #'sphere-intersect))
s pt xr yr zr))
(defun sphere-intersect (s pt xr yr zr)
(let* ((c (sphere-center s))
(n (minroot (+ (sq xr) (sq yr) (sq zr))
(* 2 (+ (* (- (x pt) (x c)) xr)
(* (- (y pt) (y c)) yr)
(* (- (z pt) (z c)) zr)))
(+ (sq (- (x pt) (x c)))
(sq (- (y pt) (y c)))
(sq (- (z pt) (z c)))
(- (sq (sphere-radius s)))))))
(if n
(make-point :x (+ (x pt) (* n xr))
:y (+ (y pt) (* n yr))
:z (+ (z pt) (* n zr))))))
(defun normal (s pt)
(funcall (typecase s (sphere #'sphere-normal))
s pt))
(defun sphere-normal (s pt)
(let ((c (sphere-center s)))
(unit-vector (- (x c) (x pt))
(- (y c) (y pt))
(- (z c) (z pt)))))
图 9.5 球体。
函数 intersect
判断与何种平面有关,并调用对应的函数。在此时只有一种, sphere-intersect
,但 intersect
是写成可以容易扩展处理别种物体。
我们要怎么找到一束光与一个球体的交点 (intersection)呢?光线是表示成点 p=⟨x0,y0,x0⟩ 以及单位向量 v=⟨xr,yr,xr⟩ 。每个在光上的点可以表示为 p+nv ,对于某个 n ── 即 ⟨x0+nxr,y0+nyr,z0+nzr⟩ 。光击中球体的点的距离至中心 ⟨xc,yc,zc⟩ 会等于球体的半径 r 。所以在下列这个交点的方程序会成立:
r=(x0+nxr−xc)2+(y0+nyr−yc)2+(z0+nzr−zc)2−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−√
这会给出
an2+bn+c=0
其中
a=x2r+y2r+z2rb=2((x0−xc)xr+(y0−yc)yr+(z0−zc)zr)c=(x0−xc)2+(y0−yc)2+(z0−zc)2−r2
要找到交点我们只需要找到这个二次方程序的根。它可能是零、一个或两个实数根。没有根代表光没有击中球体;一个根代表光与球体交于一点 (擦过 「grazing hit」);两个根代表光与球体交于两点 (一点交于进入时、一点交于离开时)。在最后一个情况里,我们想要两个根之中较小的那个; n 与光离开观测点的距离成正比,所以先击中的会是较小的 n 。所以我们调用 minroot
。如果有一个根, sphere-intersect
返回代表该点的 ⟨x0+nxr,y0+nyr,z0+nzr⟩ 。
图 9.5 的另外两个函数, normal
与 sphere-normal
类比于 intersect
与 sphere-intersect
。要找到垂直于球体很简单 ── 不过是从该点至球体中心的向量而已。
图 9.6 示范了我们如何产生图片; ray-test
定义了 38 个球体(不全都看的见)然后产生一张图片,叫做 “sphere.pgm” 。
(译注:PGM 可移植灰度图格式,更多信息参见 wiki )
(defun ray-test (&optional (res 1))
(setf *world* nil)
(defsphere 0 -300 -1200 200 .8)
(defsphere -80 -150 -1200 200 .7)
(defsphere 70 -100 -1200 200 .9)
(do ((x -2 (1+ x)))
((> x 2))
(do ((z 2 (1+ z)))
((> z 7))
(defsphere (* x 200) 300 (* z -400) 40 .75)))
(tracer (make-pathname :name "spheres.pgm") res))
图 9.6 使用光线追踪器
图 9.7 是产生出来的图片,其中 res
参数为 10。