绘制悬链线
在为 Balloon Platform Defense 制作气球之间的绳子时,我得弄清楚如何画出悬挂在两点之间的绳子,这种形状通常称为悬链线。我在网上找到的所有例子,要么给问题加了限制,比如两个点处于同一高度,要么依赖一些已知条件,比如绳子起始时的角度,而这些在这里都不知道——已知的只有绳子的起点、终点和绳长。维基百科提供了许多关于悬链线方程的信息,我就以此为起点。 悬链线的方程为:y=a\cosh \left (\frac{x}{a} \right )=\frac{a\left ( e^{\frac{x}{a}}+e^{-\frac{x}{a}} \right )}{2}
这里假设绳子的最低点位于它与 y 轴的交点。实际上,方程中的 x 和 y 还要分别加上常数,而这两个常数都有待确定,因为我们不知道最低点会在哪里——这正是它比所有教科书例子都复杂的原因之一。(你有没有发现,现实生活中的数学问题总是比教科书上的例子复杂得多?我至今还在等一个能用解析方法求出积分的现实问题。)不过这得先放一放,因为首先要确定 a 的值,也就是方程中基本上决定曲线宽窄的常数。注意:如果两点在 x 轴上的位置相同,a 就是无穷大,所以在走到这一步之前就应该处理好这种情况——甚至包括两点非常接近、a 大到无法计算的情况。(这里“大到无法计算”指的是:用作指数函数的参数时,在你所用的浮点数精度下会使其返回 NaN 或无穷大的任何数。)当然,如果两点之间的距离大于绳长,也不要让代码走到这一步——结果会很糟糕。
计算缩放系数
为了计算 a,维基百科给出了下面的方程(它基于悬链线的一个性质:cosh 给出曲线的位置,而 sinh 给出曲线的长度):\sqrt{s^{2}-v^{2}}=2a\sinh \left ( \frac{h}{2a} \right )
其中 s 是绳长,h 和 v 是起点与终点之间水平和竖直方向的距离(取绝对值)。这些都是已知量,只剩一个未知数,但必须用数值方法求解。我起初没怎么讲究初始值,结果收敛与否全凭运气。幸好,找到一个好的初始值并不难。双曲正弦的泰勒级数为:
\sinh x=x+\frac{x^{3}}{3!}+\frac{x^{5}}{5!}+\cdots
令 u = \frac{1}{4a^{2}},c = \sqrt{s^{2}-v^{2}},要解的方程就变成:
c=\frac{1}{\sqrt{u}}\sinh \left ( h\sqrt{u} \right )
取 sinh 泰勒级数的前三项并化简,得到:
c=\frac{1}{\sqrt{u}}\left (h\sqrt{u}+\frac{\left ( h\sqrt{u} \right )^{3}}{3!} +\frac{\left ( h\sqrt{u} \right )^{5}}{5!} \right )
c=\frac{1}{\sqrt{u}}\left (h\sqrt{u}+\frac{h^{3}u\sqrt{u}}{3!} +\frac{h^{5}u^{2}\sqrt{u}}{5!} \right )
c=h+\frac{h^{3}u}{3!} +\frac{h^{5}u^{2}}{5!}
整理后得到:
\frac{h^{5}}{120}u^{2}+\frac{h^{3}}{6}u+\left ( h-c \right )=0
这是一个关于 u 的简单的一元二次方程,把它的系数代入中学学过的求根公式 x = \frac{-b\pm \sqrt{b^{2}-4ac}}{2a}
就能得到 u 的一个好的初始值,进而得到 a 的初始值。
u = \frac{-\frac{1}{6}h^{3}+ \sqrt{\frac{1}{36}h^{6}-\frac{1}{30}h^{5}\left ( h-c \right )}}{\frac{1}{60}h^{5}},其中 a=\frac{1}{2\sqrt{u}}
这个初始值已经足够接近,可以用牛顿法求出解。写成 f\left ( a \right )=0
的形式,就有 f\left ( a \right )=2a \sinh \left ( \frac{h}{2a} \right )-c
以及 {f}'\left ( a \right )=2 \sinh \left ( \frac{h}{2a} \right )-\frac{h}{a}\cosh \left ( \frac{h}{2a} \right )
由于 a_{n+1}=a_{n}-\frac{f\left ( a_{n} \right )}{{f}'\left ( a_{n} \right )}
,
a_{n+1}=a_{n}-\frac{2a \sinh \left ( \frac{h}{2a} \right )-c}{2 \sinh \left ( \frac{h}{2a} \right )-\frac{h}{a}\cosh \left ( \frac{h}{2a} \right )}=a_{n}-\frac{a \sinh \left ( \frac{h}{2a} \right )-0.5c}{ \sinh \left ( \frac{h}{2a} \right )-\frac{h}{2a}\cosh \left ( \frac{h}{2a} \right )}
我目前检查数列是否收敛到与前一个值相差不超过 0.001,这通常需要 2 到 4 次迭代,不过当 x 坐标比较接近时,有时要十次以上。更高阶的 Householder 法大概也可以用,而且不会增加太多计算量,因为大部分计算都花在求双曲函数上,而函数的更高阶导数应该只会多出它们的倍数。既然缩放已经知道了,就可以计算平移了。
计算平移
标准方程y=a\cosh \left (\frac{x}{a} \right )
假设悬链线关于 y 轴对称,最低点在 (0,a),而我们要画的曲线可能在屏幕上的任何位置,所以必须求出一个把它移到正确位置的平移 (p,q),方程变为:
y-q=a\cosh \left (\frac{x-p}{a} \right )
(提醒一下:此时 a 已知,x 和 y 是变量,所以只有 p 和 q 仍是未知数。)我的第一个想法是把 x 和 y 的已知值——也就是绳子的两个端点——代入方程。理论上这是可行的,但无论我怎样尽力化简,得到的方程在计算过程中常常产生双精度浮点数无法处理的中间值。最后我只好另找办法,结果反而更高效。
还记得吗?把悬链线方程中的 cosh 换成 sinh,就得到曲线的长度。把左、右端点的 x 坐标以及要画的绳子的已知长度(s)代入这个形式,得到方程:
a\sinh \left (\frac{x_{right}-p}{a} \right )-a\sinh \left (\frac{x_{left}-p}{a} \right )=s
双曲函数的一个恒等式是:
\sinh x - \sinh y=2\cosh\left ( \frac{x+y}{2} \right ) \sinh \left ( \frac{x-y}{2} \right )
因此:
\sinh \left (\frac{x_{right}-p}{a} \right )-\sinh \left (\frac{x_{left}-p}{a} \right )=2\cosh\left ( \frac{x_{right}+x_{left}-2p}{2a} \right ) \sinh \left ( \frac{x_{right}-x_{left}}{2a} \right )
在 cosh 的参数中,左右两个 x 坐标相加再除以 2,得到的是中点的坐标,所以不妨直接用它代替。sinh 参数中的减法得到的正是前面称为 h 的值,所以这个 sinh 也可以作为已知量算出,原方程可以整理为:
\cosh\left ( \frac{x_{middle}-p}{a} \right ) = \frac{s}{2a\sinh \left ( \frac{h}{2a}\right )}
现在可以用反双曲余弦(arcosh)解出 p,但需要小心。由于 cosh 是偶函数,我们得到的总是正的答案,但还有一个负的答案,所以 p 有两个可能的值。
p =x_{middle}\pm a\cosh^{-1}\left ( \frac{s}{2a\sinh \left ( \frac{h}{2a}\right )} \right )
要判断用哪一个,可以看端点的 y 坐标。拿一段绳子试一试就会发现:在绳长和 x 坐标都相同的情况下,如果右端的 y 坐标比左端低,曲线的最低点(如果它确实落在两个 x 坐标之间)就在中点的右边(因此对应正的 arcosh),反之亦然。
还可能出现另一个问题:当两个 y 坐标相等时,曲线的最低点应该正好在中间,所以 cosh 的参数应该为零,取 arcosh 的那个值相应地应该为一。由于浮点误差,它常常算出来略小于一,在取 arcosh 时就会出错。明智的做法是在开始这一步计算之前,先检查两个 y 坐标是否相等(或几乎相等),既能省去计算,又能避免这个错误;在这种情况下,直接认为 p 正好位于两个端点的中间即可。
既然 a 和 p 都已知,只要把其中一个端点的 x 和 y 值代入方程
y-q=a\cosh \left (\frac{x-p}{a} \right )
并整理为:
q=y-a\cosh \left (\frac{x-p}{a} \right )
就能算出 q。求出 q 之后,你就得到了用来画曲线的最终方程。
y=a\cosh \left (\frac{x-p}{a} \right )+q
现在画曲线就很简单了。在计算参数的大多数步骤中,我都尽可能精确地计算双曲函数,因为很难判断误差会如何传播。但在最后这一步——它占了大部分计算量——你知道所用参数有多大、能容忍多大的误差,可以预测展开到某一次幂的泰勒级数是否足够精确。根据我的经验,大多数时候都值得使用。