<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="http://gentoo-zh.org/extern.php?action=feed&amp;tid=575&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo-zh / ANSI Common Lisp 第九章：数字]]></title>
		<link>http://www.gentoo-zh.org/viewtopic.php?id=575</link>
		<description><![CDATA[ANSI Common Lisp 第九章：数字 最近发表的帖子。]]></description>
		<lastBuildDate>Fri, 18 Nov 2022 13:10:53 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[ANSI Common Lisp 第九章：数字]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?pid=615#p615</link>
			<description><![CDATA[<p>处理数字是 Common Lisp 的强项之一。Common Lisp 有着丰富的数值类型，而 Common Lisp 操作数字的特性与其他语言比起来更受人喜爱。</p><p>&#160; &#160; &#160; &#160; 9.1 类型 (Types)<br />&#160; &#160; &#160; &#160; 9.2 转换及取出 (Conversion and Extraction)<br />&#160; &#160; &#160; &#160; 9.3 比较 (Comparison)<br />&#160; &#160; &#160; &#160; 9.4 算术 (Arithematic)<br />&#160; &#160; &#160; &#160; 9.5 指数 (Exponentiation)<br />&#160; &#160; &#160; &#160; 9.6 三角函数 (Trigometric Functions)<br />&#160; &#160; &#160; &#160; 9.7 表示法 (Representations)<br />&#160; &#160; &#160; &#160; 9.8 范例：追踪光线 (Example: Ray-Tracing)<br />&#160; &#160; &#160; &#160; Chapter 9 总结 (Summary)<br />&#160; &#160; &#160; &#160; Chapter 9 练习 (Exercises)</p><p>9.1 类型 (Types)</p><p>Common Lisp 提供了四种不同类型的数字：整数、浮点数、比值与复数。本章所讲述的函数适用于所有类型的数字。有几个不能用在复数的函数会特别说明。</p><p>整数写成一串数字：如 2001 。浮点数是可以写成一串包含小数点的数字，如 253.72 ，或是用科学表示法，如 2.5372e2 。比值是写成由整数组成的分数：如 2/3 。而复数 a+bi 写成 #c(a b) ，其中 a 与 b 是任两个类型相同的实数。</p><p>谓词 integerp 、 floatp 以及 complexp 针对相应的数字类型返回真。图 9.1 展示了数值类型的层级。</p><p>../_images/Figure-9.1.png</p><p>图 9.1: 数值类型</p><p>要决定计算过程会返回何种数字，以下是某些通用的经验法则：</p><p>&#160; &#160; 如果数值函数接受一个或多个浮点数作为参数，则返回值会是浮点数 (或是由浮点数组成的复数)。所以 (+ 1.0 2) 求值为 3.0，而 (+ #c(0 1.0) 2) 求值为 #c(2.0 1.0) 。<br />&#160; &#160; 可约分的比值会被转换成最简分数。所以 (/ 10 2) 会返回 5 。<br />&#160; &#160; 若计算过程中复数的虚部变成 0 时，则复数会被转成实数 。所以 (+ #c(1 -1) #c(2 1)) 求值成 3 。</p><p>第二、第三个规则可以在读入参数时直接应用，所以：</p><p>&gt; (list (ratiop 2/2) (complexp #c(1 0)))<br />(NIL NIL)</p><p>9.2 转换及取出 (Conversion and Extraction)</p><p>Lisp 提供四种不同类型的数字的转换及取出位数的函数。函数 float 将任何实数转换成浮点数:</p><p>&gt; (mapcar #&#039;float &#039;(1 2/3 .5))<br />(1.0 0.6666667 0.5)</p><p>将数字转成整数未必需要转换，因为它可能牵涉到某些资讯的丧失。函数 truncate 返回任何实数的整数部分:</p><p>&gt; (truncate 1.3)<br />1<br />0.29999995</p><p>第二个返回值 0.29999995 是传入的参数减去第一个返回值。(会有 0.00000005 的误差是因为浮点数的计算本身就不精确。)</p><p>函数 floor 与 ceiling 以及 round 也从它们的参数中导出整数。使用 floor 返回小于等于其参数的最大整数，而 ceiling 返回大于或等于其参数的最小整数，我们可以将 mirror? (46 页，译注: 3.11 节)改成可以找出所有回文（palindromes）的版本:</p><p>(defun palindrome? (x)<br />&#160; (let ((mid (/ (length x) 2)))<br />&#160; &#160; (equal (subseq x 0 (floor mid))<br />&#160; &#160; &#160; &#160; &#160; &#160;(reverse (subseq x (ceiling mid))))))</p><p>和 truncate 一样， floor 与 ceiling 也返回传入参数与第一个返回值的差，作为第二个返回值。</p><p>&gt; (floor 1.5)<br />1<br />0.5</p><p>实际上，我们可以把 truncate 想成是这样定义的:</p><p>(defun our-truncate (n)<br />&#160; &#160; (if (&gt; n 0)<br />&#160; &#160; &#160; &#160; (floor n)<br />&#160; &#160; &#160; &#160; (ceiling n)))</p><p>函数 round 返回最接近其参数的整数。当参数与两个整数的距离相等时， Common Lisp 和很多程序语言一样，不会往上取（round up）整数。而是取最近的偶数：</p><p>&gt; (mapcar #&#039;round &#039;(-2.5 -1.5 1.5 2.5))<br />(-2 -2 2 2)</p><p>在某些数值应用中这是好事，因为舍入误差（rounding error）通常会互相抵消。但要是用户期望你的程序将某些值取整数时，你必须自己提供这个功能。 [1] 与其他的函数一样， round 返回传入参数与第一个返回值的差，作为第二个返回值。</p><p>函数 mod 仅返回 floor 返回的第二个返回值；而 rem 返回 truncate 返回的第二个返回值。我们在 94 页（译注： 5.7 节）曾使用mod 来决定一个数是否可被另一个整除，以及 127 页（译注： 7.4 节）用来找出环状缓冲区（ring buffer）中，元素实际的位置。</p><p>关于实数，函数 signum 返回 1 、 0 或 -1 ，取决于它的参数是正数、零或负数。函数 abs 返回其参数的绝对值。因此 (* (absx) (signum x)) 等于 x 。</p><p>&gt; (mapcar #&#039;signum &#039;(-2 -0.0 0.0 0 .5 3))<br />(-1 -0.0 0.0 0 1.0 1)</p><p>在某些应用里， -0.0 可能自成一格（in its own right），如上所示。实际上功能上几乎没有差别，因为数值 -0.0 与 0.0 有着一样的行为。</p><p>比值与复数概念上是两部分的结构。(译注：像 Cons 这样的两部分结构) 函数 numerator 与 denominator 返回比值或整数的分子与分母。（如果数字是整数，前者返回该数，而后者返回 1 。）函数 realpart 与 imgpart 返回任何数字的实数与虚数部分。（如果数字不是复数，前者返回该数字，后者返回 0 。）</p><p>函数 random 接受一个整数或浮点数。这样形式的表达式 (random n) ，会返回一个大于等于 0 并小于 n 的数字，并有着与 n 相同的类型。<br />9.3 比较 (Comparison)</p><p>谓词 = 比较其参数，当数值上相等时 ── 即两者的差为零时，返回真。</p><p>&gt; (= 1 1.0)<br />T<br />&gt; (eql 1 1.0)<br />NIL</p><p>= 比起 eql 来得宽松，但参数的类型需一致。</p><p>用来比较数字的谓词为 &lt; （小于）、 &lt;= （小于等于）、 = （等于）、 &gt;= （大于等于）、 &gt; (大于) 以及 /= （不相等）。以上所有皆接受一个或多个参数。只有一个参数时，它们全返回真。</p><p>(&lt;= w x y z)</p><p>等同于二元操作符的结合（conjunction），应用至每一对参数上:</p><p>(and (&lt;= w x) (&lt;= x y) (&lt;= y z))</p><p>由于 /= 若它的两个参数不等于时会返回真，表达式</p><p>(/= w x y z)</p><p>等同于</p><p>(and (/= w x) (/= w y) (/= w z)<br />&#160; &#160; &#160;(/= x y) (/= y z) (/= y z))</p><p>特殊的谓词 zerop 、 plusp 与 minusp 接受一个参数，分别于参数 = 、 &gt; 、 &lt; 零时，返回真。虽然 -0.0 （如果实现有使用它）前面有个负号，但它 = 零，</p><p>&gt; (list (minusp -0.0) (zerop -0.0))<br />(NIL T)</p><p>因此对 -0.0 使用 zerop ，而不是 minusp 。</p><p>谓词 oddp 与 evenp 只能用在整数。前者只对奇数返回真，后者只对偶数返回真。</p><p>本节定义的谓词中，只有 = 、 /= 与 zerop 可以用在复数。</p><p>函数 max 与 min 分别返回其参数的最大值与最小值。两者至少需要给一个参数:</p><p>&gt; (list (max 1 2 3 4 5) (min 1 2 3 4 5))<br />(5 1)</p><p>如果参数含有浮点数的话，结果的类型取决于各家实现。<br />9.4 算术 (Arithematic)</p><p>用来做加减的函数是 + 与 - 。两者皆接受任何数量的参数，包括没有参数，在没有参数的情况下返回 0 。（译注: - 在没有参数的情况下会报错，至少要一个参数）一个这样形式的表达式 (- n) 返回 -n 。一个这样形式的表达式</p><p>(- x y z)</p><p>等同于</p><p>(- (- x y) z)</p><p>有两个函数 1+ 与 1- ，分别将参数加 1 与减 1 后返回。 1- 有一点误导，因为 (1- x) 返回 x-1 而不是 1-x 。</p><p>宏 incf 及 decf 分别递增与递减数字。这样形式的表达式 (incf x n) 类似于 (setf x (+ x n)) 的效果，而 (decf x n) 类似于 (setf x (- x n)) 的效果。这两个形式里，第二个参数皆是选择性给入的，缺省值为 1 。</p><p>用来做乘法的函数是 * 。接受任何数量的参数。没有参数时返回 1 。否则返回参数的乘积。</p><p>除法函数 / 至少要给一个参数。这样形式的调用 (/ n) 等同于 (/ 1 n) ，</p><p>&gt; (/ 3)<br />1/3</p><p>而这样形式的调用</p><p>(/ x y z)</p><p>等同于</p><p>(/ (/ x y) z)</p><p>注意 - 与 / 两者在这方面的相似性。</p><p>当给定两个整数时， / 若第一个不是第二个的倍数时，会返回一个比值:</p><p>&gt; (/ 365 12)<br />365/12</p><p>举例来说，如果你试着找出平均每一个月有多长，可能会有解释器在逗你玩的感觉。在这个情况下，你需要的是，对比值调用 float，而不是对两个整数做 / 。</p><p>&gt; (float 365/12)<br />30.416666</p><p>9.5 指数 (Exponentiation)</p><p>要找到 xn 调用 (expt x n) ，</p><p>&gt; (expt 2 5)<br />32</p><p>而要找到 lognx 调用 (log x n) :</p><p>&gt; (log 32 2)<br />5.0</p><p>通常返回一个浮点数。</p><p>要找到 ex 有一个特别的函数 exp ，</p><p>&gt; (exp 2)<br />7.389056</p><p>而要找到自然对数，你可以使用 log 就好，因为第二个参数缺省为 e :</p><p>&gt; (log 7.389056)<br />2.0</p><p>要找到立方根，你可以调用 expt 用一个比值作为第二个参数，</p><p>&gt; (expt 27 1/3)<br />3.0</p><p>但要找到平方根，函数 sqrt 会比较快:</p><p>&gt; (sqrt 4)<br />2.0</p><p>9.6 三角函数 (Trigometric Functions)</p><p>常量 pi 是 π 的浮点表示法。它的精度取决于各家实现。函数 sin 、 cos 及 tan 分别可以找到正弦、余弦及正交函数，其中角度以径度表示：</p><p>&gt; (let ((x (/ pi 4)))<br />&#160; &#160; (list (sin x) (cos x) (tan x)))<br />(0.7071067811865475d0 0.7071067811865476d0 1.0d0)<br />;;; 译注: CCL 1.8&#160; SBCL 1.0.55 下的结果是<br />;;; (0.7071067811865475D0 0.7071067811865476D0 0.9999999999999999D0)</p><p>这些函数都接受负数及复数参数。</p><p>函数 asin 、 acos 及 atan 实现了正弦、余弦及正交的反函数。参数介于 -1 与 1 之间（包含）时， asin 与 acos 返回实数。</p><p>双曲正弦、双曲余弦及双曲正交分别由 sinh 、 cosh 及 tanh 实现。它们的反函数同样为 asinh 、 acosh 以及 atanh 。<br />9.7 表示法 (Representations)</p><p>Common Lisp 没有限制整数的大小。可以塞进一个字（word）内存的小整数称为定长数(fixnums)。在计算过程中，整数无法塞入一个字时，Lisp 切换至使用多个字的表示法（一个大数 「bignum」）。所以整数的大小限制取决于实体内存，而不是语言。</p><p>常量 most-positive-fixnum 与 most-negative-fixnum 表示一个实现不使用大数所可表示的最大与最小的数字大小。在很多实现里，它们为：</p><p>&gt; (values most-positive-fixnum most-negative-fixnum)<br />536870911<br />-536870912<br />;;; 译注: CCL 1.8 的结果为<br />1152921504606846975<br />-1152921504606846976<br />;;; SBCL 1.0.55 的结果为<br />4611686018427387903<br />-4611686018427387904</p><p>谓词 typep 接受一个参数及一个类型名称，并返回指定类型的参数。所以，</p><p>&gt; (typep 1 &#039;fixnum)<br />T<br />&gt; (type (1+ most-positive-fixnum) &#039;bignum)<br />T</p><p>浮点数的数值限制是取决于各家实现的。 Common Lisp 提供了至多四种类型的浮点数：短浮点 short-float 、 单浮点 single-float 、双浮点 double-float 以及长浮点 long-float 。Common Lisp 的实现是不需要用不同的格式来表示这四种类型（很少有实现这么干）。</p><p>一般来说，短浮点应可塞入一个字，单浮点与双浮点提供普遍的单精度与双精度浮点数的概念，而长浮点，如果想要的话，可以是很大的数。但实现可以不对这四种类型做区别，也是完全没有问题的。</p><p>你可以指定你想要何种格式的浮点数，当数字是用科学表示法时，可以通过将 e 替换为 sfdl 来得到不同的浮点数。（你也可以使用大写，这对长浮点来说是个好主意，因为 l 看起来太像 1 了。）所以要表示最大的 1.0 你可以写 1L0 。</p><p>（译注: s 为短浮点、 f 为单浮点、 d 为双浮点、 l 为长浮点。）</p><p>在给定的实现里，用十六个全局常量标明了每个格式的限制。它们的名字是这种形式: m-s-f ，其中 m 是 most 或 least ， s 是positive 或 negative ，而 f 是四种浮点数之一。 λ</p><p>浮点数下溢（underflow）与溢出（overflow），都会被 Common Lisp 视为错误 :</p><p>&gt; (* most-positive-long-float 10)<br />Error: floating-point-overflow</p><p>9.8 范例：追踪光线 (Example: Ray-Tracing)</p><p>作为一个数值应用的范例，本节示范了如何撰写一个光线追踪器 (ray-tracer)。光线追踪是一个高级的 (deluxe)渲染算法: 它产生出逼真的图像，但需要花点时间。</p><p>要产生一个 3D 的图像，我们至少需要定义四件事: 一个观测点 (eye)、一个或多个光源、一个由一个或多个平面所组成的模拟世界 (simulated world)，以及一个作为通往这个世界的窗户的平面 (图像平面「image plane」)。我们产生出的是模拟世界投影在图像平面区域的图像。</p><p>光线追踪独特的地方在于，我们如何找到这个投影: 我们一个一个像素地沿着图像平面走，追踪回到模拟世界里的光线。这个方法带来三个主要的优势: 它让我们容易得到现实世界的光学效应 (optical effect)，如透明度 (transparency)、反射光 (reflected light)以及产生阴影 (cast shadows)；它让我们可以直接用任何我们想要的几何的物体，来定义出模拟的世界，而不需要用多边形 (polygons)来建构它们；以及它很简单实现。</p><p>(defun sq (x) (* x x))</p><p>(defun mag (x y z)<br />&#160; (sqrt (+ (sq x) (sq y) (sq z))))</p><p>(defun unit-vector (x y z)<br />&#160; (let ((d (mag x y z)))<br />&#160; &#160; (values (/ x d) (/ y d) (/ z d))))</p><p>(defstruct (point (:conc-name nil))<br />&#160; x y z)</p><p>(defun distance (p1 p2)<br />&#160; (mag (- (x p1) (x p2))<br />&#160; &#160; &#160; &#160;(- (y p1) (y p2))<br />&#160; &#160; &#160; &#160;(- (z p1) (z p2))))</p><p>(defun minroot (a b c)<br />&#160; (if (zerop a)<br />&#160; &#160; &#160; (/ (- c) b)<br />&#160; &#160; &#160; (let ((disc (- (sq b) (* 4 a c))))<br />&#160; &#160; &#160; &#160; (unless (minusp disc)<br />&#160; &#160; &#160; &#160; &#160; (let ((discrt (sqrt disc)))<br />&#160; &#160; &#160; &#160; &#160; &#160; (min (/ (+ (- b) discrt) (* 2 a))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(/ (- (- b) discrt) (* 2 a))))))))</p><p>图 9.2 实用数学函数</p><p>图 9.2 包含了我们在光线追踪器里会需要用到的一些实用数学函数。第一个 sq ，返回其参数的平方。下一个 mag ，返回一个给定 x``yz 所组成向量的大小 (magnitude)。这个函数被接下来两个函数用到。我们在 unit-vector 用到了，此函数返回三个数值，来表示与单位向量有着同样方向的向量，其中向量是由 xyz 所组成的:</p><p>&gt; (multiple-value-call #&#039;mag (unit-vector 23 12 47))<br />1.0</p><p>我们在 distance 也用到了 mag ，它返回三维空间中，两点的距离。（定义 point 结构来有一个 nil 的 conc-name 意味着栏位存取的函数会有跟栏位一样的名字: 举例来说， x 而不是 point-x 。)</p><p>最后 minroot 接受三个实数， a , b 与 c ，并返回满足等式 ax2+bx+c=0 的最小实数 x 。当 a 不为 0 时，这个等式的根由下面这个熟悉的式子给出:</p><p>x=−b±b2−4ac−−−−−−−√2a</p><p>图 9.3 包含了定义一个最小光线追踪器的代码。 它产生通过单一光源照射的黑白图像，与观测点 (eye)处于同个位置。 (结果看起来像是闪光摄影术 (flash photography)拍出来的)</p><p>surface 结构用来表示模拟世界中的物体。更精确的说，它会被 included 至定义具体类型物体的结构里，像是球体 (spheres)。surface 结构本身只包含一个栏位: 一个 color 范围从 0 (黑色) 至 1 (白色)。</p><p>(defstruct surface color)</p><p>(defparameter *world* nil)<br />(defconstant eye (make-point :x 0 :y 0 :z 200))</p><p>(defun tracer (pathname &amp;optional (res 1))<br />&#160; (with-open-file (p pathname :direction :output)<br />&#160; &#160; (format p &quot;P2 ~A ~A 255&quot; (* res 100) (* res 100))<br />&#160; &#160; (let ((inc (/ res)))<br />&#160; &#160; &#160; (do ((y -50 (+ y inc)))<br />&#160; &#160; &#160; &#160; &#160; ((&lt; (- 50 y) inc))<br />&#160; &#160; &#160; &#160; (do ((x -50 (+ x inc)))<br />&#160; &#160; &#160; &#160; &#160; &#160; ((&lt; (- 50 x) inc))<br />&#160; &#160; &#160; &#160; &#160; (print (color-at x y) p))))))</p><p>(defun color-at (x y)<br />&#160; (multiple-value-bind (xr yr zr)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(unit-vector (- x (x eye))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (- y (y eye))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (- 0 (z eye)))<br />&#160; &#160; (round (* (sendray eye xr yr zr) 255))))</p><p>(defun sendray (pt xr yr zr)<br />&#160; (multiple-value-bind (s int) (first-hit pt xr yr zr)<br />&#160; &#160; (if s<br />&#160; &#160; &#160; &#160; (* (lambert s int xr yr zr) (surface-color s))<br />&#160; &#160; &#160; &#160; 0)))</p><p>(defun first-hit (pt xr yr zr)<br />&#160; (let (surface hit dist)<br />&#160; &#160; (dolist (s *world*)<br />&#160; &#160; &#160; (let ((h (intersect s pt xr yr zr)))<br />&#160; &#160; &#160; &#160; (when h<br />&#160; &#160; &#160; &#160; &#160; (let ((d (distance h pt)))<br />&#160; &#160; &#160; &#160; &#160; &#160; (when (or (null dist) (&lt; d dist))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; (setf surface s hit h dist d))))))<br />&#160; &#160; (values surface hit)))</p><p>(defun lambert (s int xr yr zr)<br />&#160; (multiple-value-bind (xn yn zn) (normal s int)<br />&#160; &#160; (max 0 (+ (* xr xn) (* yr yn) (* zr zn)))))</p><p>图 9.3 光线追踪。</p><p>图像平面会是由 x 轴与 y 轴所定义的平面。观测者 (eye) 会在 z 轴，距离原点 200 个单位。所以要在图像平面可以被看到，插入至*worlds* 的表面 (一开始为 nil)会有着负的 z 座标。图 9.4 说明了一个光线穿过图像平面上的一点，并击中一个球体。</p><p>../_images/Figure-9.4.png</p><p>图 9.4: 追踪光线。</p><p>函数 tracer 接受一个路径名称，并写入一张图片至对应的文件。图片文件会用一种简单的 ASCII 称作 PGM 的格式写入。默认情况下，图像会是 100x100 。我们 PGM 文件的标头 (headers) 会由标签 P2 组成，伴随着指定图片宽度 (breadth)与高度 (height)的整数，初始为 100，单位为 pixel，以及可能的最大值 (255)。文件剩余的部份会由 10000 个介于 0 (黑)与 1 (白)整数组成，代表着 100 条 100 像素的水平线。</p><p>图片的解析度可以通过给入明确的 res 来调整。举例来说，如果 res 是 2 ，则同样的图像会被渲染成 200x200 。</p><p>图片是一个在图像平面 100x100 的正方形。每一个像素代表着穿过图像平面抵达观测点的光的数量。要找到每个像素光的数量，tracer 调用 color-at 。这个函数找到从观测点至该点的向量，并调用 sendray 来追踪这个向量回到模拟世界的轨迹； sandray 会返回一个数值介于 0 与 1 之间的亮度 (intensity)，之后会缩放成一个 0 至 255 的整数来显示。</p><p>要决定一个光线的亮度， sendray 需要找到光是从哪个物体所反射的。要办到这件事，我们调用 first-hit ，此函数研究在*world* 里的所有平面，并返回光线最先抵达的平面（如果有的话）。如果光没有击中任何东西， sendray 仅返回背景颜色，按惯例是 0 (黑色)。如果光线有击中某物的话，我们需要找出在光击中时，有多少数量的光照在该平面。</p><p>朗伯定律 告诉我们，由平面上一点所反射的光的强度，正比于该点的单位法向量 (unit normal vector) N (这里是与平面垂直且长度为一的向量)与该点至光源的单位向量 L 的点积 (dot-product):</p><p>i=N⋅L</p><p>如果光刚好照到这点， N 与 L 会重合 (coincident)，则点积会是最大值， 1 。如果将在这时候将平面朝光转 90 度，则 N 与 L 会垂直，则两者点积会是 0 。如果光在平面后面，则点积会是负数。</p><p>在我们的程序里，我们假设光源在观测点 (eye)，所以 lambert 使用了这个规则来找到平面上某点的亮度 (illumination)，返回我们追踪的光的单位向量与法向量的点积。</p><p>在 sendray 这个值会乘上平面的颜色 (即便是有好的照明，一个暗的平面还是暗的)来决定该点之后总体亮度。</p><p>为了简单起见，我们在模拟世界里会只有一种物体，球体。图 9.5 包含了与球体有关的代码。球体结构包含了 surface ，所以一个球体会有一种颜色以及 center 和 radius 。调用 defsphere 添加一个新球体至世界里。</p><p>(defstruct (sphere (:include surface))<br />&#160; radius center)</p><p>(defun defsphere (x y z r c)<br />&#160; (let ((s (make-sphere<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160;:radius r<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160;:center (make-point :x x :y y :z z)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160;:color&#160; c)))<br />&#160; &#160; (push s *world*)<br />&#160; &#160; s))</p><p>(defun intersect (s pt xr yr zr)<br />&#160; (funcall (typecase s (sphere #&#039;sphere-intersect))<br />&#160; &#160; &#160; &#160; &#160; &#160;s pt xr yr zr))</p><p>(defun sphere-intersect (s pt xr yr zr)<br />&#160; (let* ((c (sphere-center s))<br />&#160; &#160; &#160; &#160; &#160;(n (minroot (+ (sq xr) (sq yr) (sq zr))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(* 2 (+ (* (- (x pt) (x c)) xr)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(* (- (y pt) (y c)) yr)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(* (- (z pt) (z c)) zr)))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(+ (sq (- (x pt) (x c)))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (sq (- (y pt) (y c)))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (sq (- (z pt) (z c)))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (- (sq (sphere-radius s)))))))<br />&#160; &#160; (if n<br />&#160; &#160; &#160; &#160; (make-point :x&#160; (+ (x pt) (* n xr))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; :y&#160; (+ (y pt) (* n yr))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; :z&#160; (+ (z pt) (* n zr))))))</p><p>(defun normal (s pt)<br />&#160; (funcall (typecase s (sphere #&#039;sphere-normal))<br />&#160; &#160; &#160; &#160; &#160; &#160;s pt))</p><p>(defun sphere-normal (s pt)<br />&#160; (let ((c (sphere-center s)))<br />&#160; &#160; (unit-vector (- (x c) (x pt))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(- (y c) (y pt))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(- (z c) (z pt)))))</p><p>图 9.5 球体。</p><p>函数 intersect 判断与何种平面有关，并调用对应的函数。在此时只有一种， sphere-intersect ，但 intersect 是写成可以容易扩展处理别种物体。</p><p>我们要怎么找到一束光与一个球体的交点 (intersection)呢？光线是表示成点 p=⟨x0,y0,x0⟩ 以及单位向量 v=⟨xr,yr,xr⟩ 。每个在光上的点可以表示为 p+nv ，对于某个 n ── 即 ⟨x0+nxr,y0+nyr,z0+nzr⟩ 。光击中球体的点的距离至中心 ⟨xc,yc,zc⟩ 会等于球体的半径 r 。所以在下列这个交点的方程序会成立:</p><p>r=(x0+nxr−xc)2+(y0+nyr−yc)2+(z0+nzr−zc)2−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−√</p><p>这会给出</p><p>an2+bn+c=0</p><p>其中</p><p>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</p><p>要找到交点我们只需要找到这个二次方程序的根。它可能是零、一个或两个实数根。没有根代表光没有击中球体；一个根代表光与球体交于一点 (擦过 「grazing hit」)；两个根代表光与球体交于两点 (一点交于进入时、一点交于离开时)。在最后一个情况里，我们想要两个根之中较小的那个； n 与光离开观测点的距离成正比，所以先击中的会是较小的 n 。所以我们调用 minroot 。如果有一个根， sphere-intersect 返回代表该点的 ⟨x0+nxr,y0+nyr,z0+nzr⟩ 。</p><p>图 9.5 的另外两个函数， normal 与 sphere-normal 类比于 intersect 与 sphere-intersect 。要找到垂直于球体很简单 ── 不过是从该点至球体中心的向量而已。</p><p>图 9.6 示范了我们如何产生图片； ray-test 定义了 38 个球体（不全都看的见）然后产生一张图片，叫做 “sphere.pgm” 。</p><p>(译注：PGM 可移植灰度图格式，更多信息参见 wiki )</p><p>(defun ray-test (&amp;optional (res 1))<br />&#160; (setf *world* nil)<br />&#160; (defsphere 0 -300 -1200 200 .8)<br />&#160; (defsphere -80 -150 -1200 200 .7)<br />&#160; (defsphere 70 -100 -1200 200 .9)<br />&#160; (do ((x -2 (1+ x)))<br />&#160; &#160; &#160; ((&gt; x 2))<br />&#160; &#160; (do ((z 2 (1+ z)))<br />&#160; &#160; &#160; &#160; ((&gt; z 7))<br />&#160; &#160; &#160; (defsphere (* x 200) 300 (* z -400) 40 .75)))<br />&#160; (tracer (make-pathname :name &quot;spheres.pgm&quot;) res))</p><p>图 9.6 使用光线追踪器</p><p>图 9.7 是产生出来的图片，其中 res 参数为 10。</p><p>../_images/Figure-9.7.png</p><p>图 9.7: 追踪光线的图</p><p>一个实际的光线追踪器可以产生更复杂的图片，因为它会考虑更多，我们只考虑了单一光源至平面某一点。可能会有多个光源，每一个有不同的强度。它们通常不会在观测点，在这个情况程序需要检查至光源的向量是否与其他平面相交，这会在第一个相交的平面上产生阴影。将光源放置于观测点让我们不需要考虑这麽复杂的情况，因为我们看不见在阴影中的任何点。</p><p>一个实际的光线追踪器不仅追踪光第一个击中的平面，也会加入其它平面的反射光。一个实际的光线追踪器会是有颜色的，并可以模型化出透明或是闪耀的平面。但基本的算法会与图 9.3 所演示的差不多，而许多改进只需要递回的使用同样的成分。</p><p>一个实际的光线追踪器可以是高度优化的。这里给出的程序为了精简写成，甚至没有如 Lisp 程序员会最佳化的那样，就仅是一个光线追踪器而已。仅加入类型与行内宣告 (13.3 节)就可以让它变得两倍以上快。<br />Chapter 9 总结 (Summary)</p><p>&#160; &#160; Common Lisp 提供整数 (integers)、比值 (ratios)、浮点数 (floating-point numbers)以及复数 (complex numbers)。<br />&#160; &#160; 数字可以被约分或转换 (converted)，而它们的位数 (components)可以被取出。<br />&#160; &#160; 用来比较数字的谓词可以接受任意数量的参数，以及比较下一数对 (successive pairs) ── /= 函数除外，它是用来比较所有的数对 (pairs)。<br />&#160; &#160; Common Lisp 几乎提供你在低阶科学计算机可以看到的数值函数。同样的函数普遍可应用在多种类型的数字上。<br />&#160; &#160; Fixnum 是小至可以塞入一个字 (word)的整数。它们在必要时会悄悄但花费昂贵地转成大数 (bignum)。Common Lisp 提供最多四种浮点数。每一个浮点表示法的限制是实现相关的 (implementation-dependent)常量。<br />&#160; &#160; 一个光线追踪器 (ray-tracer)通过追踪光线来产生图像，使得每一像素回到模拟的世界。</p><p>Chapter 9 练习 (Exercises)</p><p>&#160; &#160; 定义一个函数，接受一个实数列表，若且唯若 (iff)它们是非递减 (nondecreasing)顺序时返回真。<br />&#160; &#160; 定义一个函数，接受一个整数 cents 并返回四个值，将数字用 25- , 10- , 5- , 1- 来显示，使用最少数量的硬币。(译注: 25- 是 25 美分，以此类推)<br />&#160; &#160; 一个遥远的星球住着两种生物， wigglies 与 wobblies 。 Wigglies 与 wobblies 唱歌一样厉害。每年都有一个比赛来选出十大最佳歌手。下面是过去十年的结果:</p><p>YEAR&#160; &#160; 1&#160; &#160; 2&#160; &#160; 3&#160; &#160; 4&#160; &#160; 5&#160; &#160; 6&#160; &#160; 7&#160; &#160; 8&#160; &#160; 9&#160; &#160; 10<br />WIGGLIES&#160; &#160; 6&#160; &#160; 5&#160; &#160; 6&#160; &#160; 4&#160; &#160; 5&#160; &#160; 5&#160; &#160; 4&#160; &#160; 5&#160; &#160; 6&#160; &#160; 5<br />WOBBLIES&#160; &#160; 4&#160; &#160; 5&#160; &#160; 4&#160; &#160; 6&#160; &#160; 5&#160; &#160; 5&#160; &#160; 6&#160; &#160; 5&#160; &#160; 4&#160; &#160; 5</p><p>写一个程序来模拟这样的比赛。你的结果实际上有建议委员会每年选出 10 个最佳歌手吗？</p><p>&#160; &#160; 定义一个函数，接受 8 个表示二维空间中两个线段端点的实数，若线段没有相交，则返回假，或返回两个值表示相交点的 x 座标与 y 座标。<br />&#160; &#160; 假设 f 是一个接受一个 (实数) 参数的函数，而 min 与 max 是有着不同正负号的非零实数，使得 f 对于参数 i 有一个根 (返回零)并满足 min &lt; i &lt; max 。定义一个函数，接受四个参数， f , min , max 以及 epsilon ，并返回一个 i 的近似值，准确至正负 epsilon 之内。<br />&#160; &#160; Honer’s method 是一个有效率求出多项式的技巧。要找到 ax3+bx2+cx+d 你对 x(x(ax+b)+c)+d 求值。定义一个函数，接受一个或多个参数 ── x 的值伴随着 n 个实数，用来表示 (n-1) 次方的多项式的系数 ── 并用 Honer’s method 计算出多项式的值。</p><p>译注: Honer’s method on wiki</p><p>&#160; &#160; 你的 Common Lisp 实现使用了几个位元来表示定长数？<br />&#160; &#160; 你的 Common Lisp 实现提供几种不同的浮点数？</p><p>脚注</p><p>[1] | 当 format 取整显示时，它不保证会取成偶数或奇数。见 125 页 (译注: 7.4 节)。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Fri, 18 Nov 2022 13:10:53 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?pid=615#p615</guid>
		</item>
	</channel>
</rss>
