代码中如何利用叉积判断三次贝塞尔曲线段的平坦度?
问题背景
我写了一个递归脚本,用来计算三次贝塞尔曲线的点并通过线段连接绘制曲线,核心需求是当曲线段足够平坦时停止递归,直接用直线代替绘制。
一开始我参考数学教程写了判断平坦度的代码,通过计算两个控制点到曲线首尾连线的垂直距离,只要两个距离都小于设定阈值,就认为曲线段足够平坦:
function check_flatness($p) { // 首尾点的向量(终点减起点) $a_x = $p[6] - $p[0]; $a_y = $p[7] - $p[1]; // 第一个控制点到起点的向量 $b_x = $p[2] - $p[0]; $b_y = $p[3] - $p[1]; // 第二个控制点到终点的向量 $c_x = $p[4] - $p[6]; $c_y = $p[5] - $p[7]; // 计算第一个控制点到首尾连线的垂直距离 $distance_1 = abs(($a_x * $b_y - $b_x * $a_y) / sqrt(($a_x * $a_x) + ($a_y * $a_y))); // 计算第二个控制点到首尾连线的垂直距离 $distance_2 = abs(($a_x * $c_y - $c_x * $a_y) / sqrt(($a_x * $a_x) + ($a_y * $a_y))); if ($distance_1 < $some_value && $distance_2 < $some_value) { return true; // 停止递归 } else { return false; } }
这段代码能正常工作,但后来在Stack Overflow上看到一种更高效的实现——不用开方和除法,纯靠叉积计算,很适合我要处理数百条曲线的场景,但我搞不懂它的原理,需要入门级的解释。
高效实现的代码
先贴出这段高效代码:
function check_flatness($p) { // 首尾点的向量(终点减起点) $a_x = $p[6] - $p[0]; $a_y = $p[7] - $p[1]; // 第一个控制点到起点的向量 $b_x = $p[2] - $p[0]; $b_y = $p[3] - $p[1]; // 第二个控制点到终点的向量 $c_x = $p[4] - $p[6]; $c_y = $p[5] - $p[7]; // 核心计算部分 $a_cross_b = ($a_x * $b_y) - ($a_y * $b_x); $a_cross_c = ($a_x * $c_y) - ($a_y * $c_x); $d_sq = ($a_x * $a_x) + ($a_y * $a_y); $flatness_sq = 0.25; return max($a_cross_b * $a_cross_b, $a_cross_c * $a_cross_c) < ($flatness_sq * $d_sq); }
原理拆解:从原方法到高效方法的转化
其实这段高效代码只是把原方法的数学公式做了等价变形,目的是避免耗时的开方和除法运算,核心逻辑和原代码完全一致,我们一步步拆解:
1. 回忆垂直距离的数学公式
原代码里计算垂直距离的公式是:
distance = |向量a × 向量b| / |向量a|
这里的×是2D向量的叉积,结果是一个标量,等于a_x*b_y - a_y*b_x;|向量a|是向量a的长度,计算方式是sqrt(a_x² + a_y²)。
这个公式的本质是:叉积的绝对值等于向量a的长度 × 控制点到首尾连线的垂直距离,所以反过来就能算出垂直距离。
2. 去掉开方和除法:公式平方变形
原代码的判断条件是distance < $some_value,我们把这个不等式两边同时平方(因为两边都是非负数,平方后不等号方向不变):
distance² < $some_value²
把distance的公式代入进去:
(|a×b| / |a|)² < $some_value²
展开后得到:
(a×b)² / |a|² < $some_value²
再把分母|a|²移到右边(同样,两边都是正数,不等号方向不变):
(a×b)² < $some_value² × |a|²
3. 对应到高效代码的变量
现在看高效代码里的变量:
$a_cross_b就是a×b(叉积的结果),$a_cross_b * $a_cross_b就是(a×b)²$d_sq是a_x² + a_y²,也就是|a|²(向量a长度的平方)$flatness_sq就是$some_value²,比如原代码里如果$some_value是0.5,那平方后就是0.25,和代码里的数值完全对应
4. 最终的判断逻辑
原代码需要同时满足两个控制点的距离都小于阈值,也就是:
(a×b)² < $some_value²×|a|² 且 (a×c)² < $some_value²×|a|²
等价于两个平方值的最大值小于$some_value²×|a|²,所以高效代码里用max(...) < (...)来做判断,逻辑和原代码完全一致。
为什么高效?
开方(sqrt())和除法是相对耗时的运算,尤其是要处理数百条曲线时,重复执行这些操作会累积性能消耗。而高效代码里只有乘法和取最大值的操作,运算速度快很多,同时完全保留了原判断逻辑的准确性。
内容的提问来源于stack exchange,提问作者Noel Whitemore

