尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

向量叉乘全解析:方向、模长、有向面积与法线计算

向量叉乘全解析:方向、模长、有向面积与法线计算 前段时间帮一个做游戏的朋友排查渲染问题模型一半发黑光照方向明明是从正面打过来的怎么调材质都不对。最后发现问题出在面片法线上——他算三角形法线时把两条边的叉乘顺序写反了法线整体翻了个面光照自然算成了背面。这个 bug 特别典型因为它暴露了一件事点乘能告诉你两个向量有多像但没法告诉你它们张成的平面朝向哪边也没法告诉你旋转是左转还是右转。补上这块拼图的正是向量叉乘。它的几何意义和模的计算是所有三维空间问题绕不过去的一环——做图形学、做机器人运动学、做物理引擎、写几何算法甚至只是想算个多边形面积都得靠它。这篇内容我打算从为什么需要它一直讲到代码里怎么用不出错。不管你是刚学线性代数、被行列式符号绕晕的学生还是工作几年、每次写叉乘都要翻文档确认一下顺序的开发者看完应该都能把它彻底钉死在脑子里。核心就三件事叉乘结果向量的方向怎么定、它的模为什么等于面积、以及怎么把它算对。1. 一个法线方向反了的光照 bug点乘为什么不够用先把这个 bug 拆开看。三维空间里三个点 A、B、C 构成一个三角形我们想求它所在平面的法向量。常规做法是取两条边 AB 和 AC做叉乘得到的就是垂直于这个平面的向量。如果我把顺序写成 AC × AB结果会整体反向。方向一反光照模型里dot(normal, lightDir)就变成负数像素直接被判为背光于是黑的。整个过程里没有任何除法、没有开方、没有一点数值不稳定唯一的问题就是顺序。而顺序这件事点乘是管不了的。1.1 点乘回答的是投影类问题先把点乘的边界划清楚才能看出叉乘补了什么。点乘a·b |a||b|cosθ结果是一个标量。它擅长回答这几类问题两个向量夹角多少、一个向量在另一个方向上的投影有多长、两个方向是不是垂直、一个点相对于一条直线或一个平面在正侧还是负侧。但注意它的盲区。余弦函数是个偶函数cosθ cos(−θ)所以点乘分不清左右。a 到 b 转 30 度和转 −30 度点乘结果一模一样。用点乘你永远不知道顺时针还是逆时针、在左边还是右边。另外点乘的结果是标量它天然丢掉了垂直于这两个向量的那个方向的信息——因为在那个方向上的投影本来就是零点乘对它是完全不敏感的。这正是三维几何里最容易出问题的地方一个平面有正反两面点乘看不见这个区别。1.2 叉乘补上的三块信息叉乘a × b返回的是一个向量它一次性补上了三块点乘给不了的信息。第一块是垂直方向。a × b得到的新向量同时垂直于 a 和 b也就是垂直于这两个向量张成的整个平面前提是它们不共线。这是求法线这件事的数学根基。第二块是有向面积的大小。这个新向量的长度等于以 a、b 为邻边张成的平行四边形的面积。注意是有向的面积本身是正的但方向带着正负号的含义翻转顺序面积数值不变、方向相反。第三块是手性/旋转方向。因为结果带方向它天然编码了从 a 转到 b 是往哪边转。二维平面上这个信息退化成一个带符号的标量正数代表逆时针、负数代表顺时针在标准右手坐标系下这是凸包算法、点在线段哪一侧判断、多边形绕序检测的核心。1.3 把它理解成有向面积向量我个人觉得最好用的一个直觉模型是叉乘把一个有向面积从标量升维成了一个向量。两个向量张成的平行四边形它有一个大小面积也有一个朝向法线指向哪边。普通面积是个正数只能表达大小而叉乘把这个面积连同朝向一起打包编码进一个三分量向量里。打个生活化的比方。你拿一本书摊开书页是一个平面面积是它的大小书朝上还是朝下是它的朝向。如果只告诉你面积多少平方厘米你没法判断书是正面朝上还是背面朝上而叉乘结果这个向量相当于同时给了你面积和哪面朝上两个信息。这就是为什么在图形学里法线不仅要归一化还要保证方向对——方向错了几何上面积一样大但物理含义完全反了。再往深一层看二维里的叉乘其实是一个学生都很熟的东西a × b a1·b2 − a2·b1这就是二阶行列式几何上就是两个二维向量张成的平行四边形的有向面积。三维叉乘是它的自然推广——把有向面积从一个数扩展成一个带方向的量。2. 三阶行列式写法叉乘的分量规则与记忆方法理解了它是什么之后下一步是它怎么算。很多人的痛点是记不住公式或者记混了符号。我自己教别人时从来不让死记八个乘积而是从一个行列式出发理解性地记。2.1 分量形式的标准定义设 a (a1, a2, a3)b (b1, b2, b3)那么a × b ( a2·b3 − a3·b2, a3·b1 − a1·b3, a1·b2 − a2·b1 )这三个分量看起来各自独立其实有非常强的规律。第一个分量里没有 a1、b1第二个分量里没有 a2、b2第三个分量里没有 a3、b3。而且每一项都是一个数乘另一个减反过来。背的时候可以这样记求第 i 个分量就把第 i 个位置挖掉剩下的四个数按交叉相减组合符号按循环顺序处理。2.2 为什么可以用三阶行列式来记把单位向量 i、j、k 放在第一行a 放第二行b 放第三行写成一个三阶行列式| i j k | | a1 a2 a3 | | b1 b2 b3 |按第一行展开i·(a2b3 − a3b2) − j·(a1b3 − a3b1) k·(a1b2 − a2b1)。整理一下第二项的符号−j·(a1b3 − a3b1) j·(a3b1 − a1b3)正好和上面的标准定义对上。这个记法的好处是你永远不会漏项。行列式展开的代数余子式规则是死的把 a、b 放好按部就班展开就行。坏处是容易在 j 前面那个负号上栽跟头——中间项是减不是加。我见过太多人把中间项写成 j(a1b3 − a3b1)结果 y 分量符号整体反了。避坑办法很简单算完之后立刻做一次垂直性检查后面会讲一步点乘就能验出来。2.3 手算的推荐顺序与符号陷阱如果不用行列式、纯手算我推荐这个顺序先把六个乘积列出来a2b3、a3b2、a3b1、a1b3、a1b2、a2b1然后两两相减减的顺序固定为前减后a2b3 − a3b2、a3b1 − a1b3、a1b2 − a2b1。这样不会乱。一个常见的心算陷阱是下标循环顺序。注意是a3b1 − a1b3不是a1b3 − a3b1。判断方法看下标第一个乘法的下标是 (3,1)第二个是 (1,3)(1,2,3)的循环里 3 后面接 1是顺着的。如果你把它写成a1b3 − a3b1符号就反了整个 y 分量跟着翻转法线方向也就反了——回到开头那个 bug 的成因。2.4 反交换律、自乘为零与分配律光会算分量还不够真正让叉乘好用的是一组代数性质理解了这些很多结论不用推导就能推出来。反交换律a × b −(b × a)。交换顺序结果整体反向。这在几何上就是法线翻面这也是开头 bug 的直接原因。自叉乘为零a × a 0零向量。两个相同的向量张不出面积也定不出唯一法线方向结果是零向量。这条性质反过来就是判断共线的工具如果a × b 0那么 a 和 b 共线平行或反向。分配律a × (b c) a × b a × c。这让叉乘可以和线性运算结合也是它能被写成一个矩阵乘法反对称矩阵的原因。与数乘兼容(ka) × b k(a × b)。缩放一个向量面积跟着按比例缩放方向不变k 为负时反向。不满足结合律(a × b) × c ≠ a × (b × c)一般不等。这一点在推导二重叉乘公式时特别重要正确的展开式是a × (b × c) b(a·c) − c(a·b)属于BAC − CAB型别自己瞎猜。把这些性质放在一起看你会发现叉乘和点乘的性格完全不同点乘是对称的、结果退化成标量叉乘是反对称的、结果保留方向。这一对称一反对称正好覆盖了三维几何的两个正交侧面。3. 结果向量的几何身份它为什么必然垂直于原平面叉乘结果垂直于两个原向量这句话几乎每本教材都会写但很多人是当结论背下来的没想过为什么要垂直。其实这件事有个非常漂亮的验证方式而且这个验证顺手就能当代码的自测手段用。3.1 用点乘验证垂直性判垂直最直接的办法做点乘看是不是零。拿c a × b的第一个分量来验a·ca·c a1(a2b3 − a3b2) a2(a3b1 − a1b3) a3(a1b2 − a2b1)展开 a1a2b3 − a1a3b2 a2a3b1 − a1a2b3 a1a3b2 − a2a3b1逐项消掉结果正好是 0。同理b·c 0。这说明 c 同时垂直于 a 和 b。这个结论在工程上太有用了。我在写几何库的时候cross函数的单元测试里必有一条随机生成两个向量算叉乘然后验证结果和两个输入向量的点乘都接近零。周期性地跑一下这个测试就能抓住符号写反、下标写错之类的低级错误。前面说的那个渲染 bug如果当时有这么一条断言能在十分钟内定位而不是查一晚上。3.2 从行列式的角度看垂直性还有一个更数学的解释理解了会更有底气。三阶行列式det(a, b, c)的几何意义是三个向量张成的平行六面体的有向体积。如果把 c 取成a × b那么det(a, b, a×b)等于|a × b|²是个正数这个恒等式后面还会用到。反过来想如果 c 不垂直于 a、b 张成的平面它就可以沿垂直于平面的方向再分解出一个分量c⊥那么det(a, b, c) det(a, b, c∥ c⊥) det(a, b, c⊥)体积会被这个垂直分量放大。叉乘给出的恰好是使得det(a, b, a×b)等于|a×b|²的那个向量说明它没有任何多余的垂直分量它本身就是垂直于平面的。这个视角在讲混合积、二重叉乘、Gram 行列式的时候特别顺。3.3 共线时会发生什么如果 a 和 b 共线平行或反向a × b 0结果是一个零向量。零向量是方向未定义的你不能对它做归一化——除以零会得到 NaN 或者无限大很多程序崩溃就崩在这一步。实际场景里这个坑出现得非常频繁。比如用三个点算三角形法线如果这三个点几乎共线退化三角形、或者点是从浮点数据里出来的、有微小噪声叉乘结果的模会接近零但不严格等于零归一化之后方向会疯狂抖动表现为光照闪烁、物理约束不稳定。我处理这个问题的固定套路是先算模判断模是否小于一个阈值比如 1e-8 乘以两条边的长度乘积小于就直接丢弃或标记为退化。不要用是否等于零来判断浮点数下几乎永远不会精确等于零。4. 模长就是面积|a||b|sinθ 的完整来历这是整个叉乘里最漂亮的一个结论也是最实用的一个叉乘结果的模等于两个向量张成的平行四边形的面积。我们把它从头推一遍推完你就不会忘了。4.1 平行四边形面积 底 × 高设 a、b 的夹角为 θ取 0 到 π 之间。以 a 为底底长是 |a|。b 在这个底方向上的高是多少把 b 投影到垂直于 a 的方向上长度是|b|sinθ。所以平行四边形面积S |a| · |b| · sinθ。当 θ 0 或 π 时两向量共线面积为零与sinθ 0一致。当 θ π/2 时面积最大等于|a||b|。整条曲线非常符合直觉。4.2 用拉格朗日恒等式推导上面的推导依赖面积 底 × 高这个几何事实。如果你想看纯代数的推导用拉格朗日恒等式更严谨|a × b|² |a|²|b|² − (a·b)²这个恒等式可以直接把叉乘的三个分量平方和展开和右边逐项对比能对上。再用a·b |a||b|cosθ|a × b|² |a|²|b|² − |a|²|b|²cos²θ |a|²|b|²(1 − cos²θ) |a|²|b|²sin²θ两边开方因为在 θ ∈ [0, π] 时 sinθ ≥ 0所以|a × b| |a||b|sinθ推导完成。这条公式还有一个副产品它给出了一种算面积不用碰三角函数的方法。直接算叉乘模比先求夹角再求 sin 要快、要稳而且不会因为角度接近 0 或 π 而出现精度灾难。4.3 三角形与多边形面积平行四边形面积除以 2 就是三角形面积S△ABC |AB × AC| / 2这是三维空间里求三角形面积最通用的公式二维、三维都适用。二维时叉乘退化成标量取绝对值除以二三维时算向量模除以二。多边形面积同样可以靠它。二维任意简单多边形顶点按顺序给出有向面积是2S (x1y2 − x2y1) (x2y3 − x3y2) ... (xn y1 − x1 yn)每一项都是相邻两个顶点坐标的二维叉乘。这个公式的妙处在于有向面积的正负直接告诉你顶点的绕序结果是正的说明逆时针负的是顺时针。这在判断多边形是否自相交、是否要翻转顶点顺序时一步就能搞定。三维多边形比如地形网格的一片则可以取一个参考点把多边形拆成扇形三角形分别算叉乘再求和。4.4 为什么用叉乘算面积比用角度更稳我这里插一个实践经验。有些教程会教你用两边长度乘 sin 夹角来算面积。实测下来这个方法在数值上明显不如直接算叉乘模原因是先算夹角需要做除法点乘除以模长乘积得到 cosθ再取 arccos 得到 θ再算 sinθ。每一步都引入误差尤其当夹角接近 0 或 π 时cosθ 接近 ±1arccos 的导数趋于无穷输入上一个极小的扰动会引起角度上的大误差。而叉乘模是纯乘加运算没有除法、没有反三角函数条件数好得多。所以在实际代码里能算叉乘就算叉乘别绕道去算角度。5. 方向由右手定则决定左右手坐标系的坑模解决了多大接下来是朝哪边。这一步在数学上简单在工程上却是翻车重灾区因为它和坐标系约定绑死。5.1 右手定则的正确操作判断a × b的方向用右手四指从 a 的方向弯向 b 的方向沿小于 180 度的那个角弯大拇指指向就是a × b的方向。用标准基向量验证一下i × j kj × k ik × i j。右手从 x 轴弯向 y 轴大拇指指向 z 轴对得上。同时j × i −k顺序反了方向就反了。有一个容易忽略的细节右手定则的弯向是沿小于 180 度的角所以对同一对向量不管你怎么选参考方向是唯一确定的。也就是说a × b方向唯一不依赖于你用什么手势只依赖于坐标系是右手系还是左手系。5.2 左手坐标系下会发生什么如果坐标系是左手系比如某些图形 API 用的是左手系z 轴指向屏幕里那么公式算出来的向量在几何上会指向另一边。这时候很多引擎会做一次轴向翻转来适配。这就是为什么你在不同的引擎、不同的库之间搬代码时法线会莫名其妙变反。解决办法不是去改公式而是明确你的项目用的是什么手系然后统一约定。我自己的习惯是在项目文档里写死本项目使用右手坐标系所有法线按逆时针绕序计算然后在几何库的叉乘函数注释里也标一遍。看似啰嗦但能省掉大量联调时间。判断手系的经验方法取 x 轴单位向量、y 轴单位向量做叉乘。如果结果等于 z 轴单位向量是右手系如果等于 −z 轴单位向量是左手系或者说明你的基向量定义和标准不同。一行代码就能验证别靠猜。5.3 共线、共面的判断方案有了叉乘几个经典的几何判定变得非常干净我把它们整理成一张表方便对照使用判断目标计算方法判据两向量是否平行a × b模接近 0三点是否共线AB × AC模接近 0四点是否共面AB × AC与AD做点乘结果接近 0点 P 在线段 AB 的哪侧二维AB × AP的符号正为左侧负为右侧三角形的法线AB × AC后归一化垂直于三角面注意这里全部用接近 0而不是等于 0原因前面讲过浮点运算下精确零基本不存在。还有一个细节判断四点共面时用的是混合积即(AB × AC) · AD 0这个式子的几何意义是平行六面体体积为零等价于四个点压平在一个平面上。混合积在计算几何里出现频率比你想的高得多。6. 落到代码里实现、容差与三个必踩的坑理论讲完了我们来看工程实现。这一节的内容是我在不同项目里反复验证过的东西很多是文档里不会写的。6.1 三种实现方式与选择最省事的是直接用现成库。Python 里numpy.cross(a, b)注意它默认作用在最后一个轴上所以np.cross对形状是(N, 3)的数组可以一次性算出 N 个叉乘批量处理非常方便。C 里如果是 Eigen用a.cross(b)。Unity 里是Vector3.Cross(a, b)。需要自己手写的时候我建议这样组织可读性和正确性兼顾def cross(a, b): ax, ay, az a bx, by, bz b return ( ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx, )注意每一行都是前减后顺序固定。写成这样比np.cross慢不了多少但省掉了依赖也方便你在不能引入大库的环境里用。二维版本更简单返回一个标量def cross2d(a, b): # 返回有向面积的两倍 return a[0] * b[1] - a[1] * b[0]很多人第一次用会踩坑以为二维叉乘也返回向量。不是的二维的叉乘结果是一个标量代表有向面积。需要的是长度时取绝对值。6.2 浮点误差与容差阈值这是最容易被忽略、也最容易引发难查 bug 的地方。当两个向量几乎平行时a × b的每个分量都是两个相近大数相减会发生灾难性抵消相对误差可能非常大。如果此时你再除以模长归一化方向会剧烈抖动。我的固定做法是写一个辅助函数import math EPS 1e-9 def safe_normalize(v): length math.sqrt(v[0]*v[0] v[1]*v[1] v[2]*v[2]) if length EPS: return None # 或返回默认方向由调用方决定 return (v[0]/length, v[1]/length, v[2]/length)这里有个细节值得强调EPS 取绝对的 1e-9 在数值范围大的场景下是不够的因为向量本身可能很大。更稳的判据是相对阈值比如先算|a|、|b|判断|a × b| / (|a| * |b|)是否小于某个小量这个比值其实就是sinθ物理意义清晰量纲也对。6.3 零向量与退化输入的防御永远不要假设输入是正常的。用户点三个点求法线他给你三个重合的点、或者三点共线这不是他的错是你要兜住。我处理的顺序是先算叉乘再判断模是否退化最后才归一化并且明确把退化情况返回给调用方处理而不是偷偷返回一个(0, 0, 1)蒙混过去。悄悄兜底看起来友好实际会让问题在更远的地方爆发——比如物理引擎里一个本该报错的面片被塞了个假法线结果整个物体开始抽搐。6.4 三种实现方式的横向对比方式优点需要注意库函数numpy/Eigen/引擎内置代码短经过充分测试注意轴约定、手系、是否支持批量手写三分量零依赖可读易调试符号容易写反必须配单元测试反对称矩阵相乘便于向量化和推导计算量略大理解成本高如果项目里需要频繁对大量向量做叉乘用反对称矩阵形式可以写成矩阵乘法配合向量化指令能跑得很快把 a 写成矩阵[0, −a3, a2; a3, 0, −a1; −a2, a1, 0]乘上 b就是a × b。这个形式在推导旋转、李代数、惯性张量时特别常见。7. 叉乘的主战场从点到直线距离到力矩计算最后说说这东西到底用在哪。你会发现它出现的频率远超想象。7.1 点到直线的距离平面上三维空间同理点 P 到直线 AB 的距离d |AB × AP| / |AB|原理很直观AB × AP的模等于以 AB 和 AP 为邻边的平行四边形面积也就是底 × 高。底是|AB|那高就是面积除以底正好是点到直线的距离。这个方法不需要求垂足坐标一步叉乘加一次除法就完事比联立方程解交点再算距离简洁太多。我在做碰撞检测的粗筛阶段判断线段之间的最近距离用的就是这套。7.2 法线与平滑法线渲染里每个面片的法线就是两条边的叉乘归一化。更进阶一点做平滑着色时顶点法线是共享该顶点的所有面片法线的加权平均。这里权重的选择有讲究简单平均在网格疏密不均的时候会有明显瑕疵用面积加权权重取面片面积通常效果更自然如果想更讲究用角度加权权重取该顶点在面片中的内角效果最好。角度加权的好处是它对细长三角形不敏感这点在做低模或重拓扑后的网格上特别明显。需要注意的坑是所有面片的法线方向必须一致。如果有的面片因为顶点绕序不同导致法线反了加权平均会互相抵消得到一塌糊涂的结果。所以我通常先做一遍法线一致化遍历相邻面片方向相反就翻转再做平均。7.3 力矩、角动量与洛伦兹力物理里叉乘出现的密度更高。力矩τ r × Fr 是从转轴指向力的作用点的向量F 是力。叉乘在这里的作用是只取力的切向分量——沿着 r 方向的力不产生转动垂直方向的力全部贡献到转矩上而|r||F|sinθ恰好就是这个切向效果。角动量L r × p磁力F qv × B这些都是叉乘结构。物理里凡是绕某点转动或者垂直作用于当前方向的效应基本都是叉乘。理解了几何意义这些公式就不用硬背了叉乘的结果只在垂直于输入的方向上有分量这就是转动效应和侧向力的数学来源。7.4 凸包、绕序与点线侧向判断二维场景下叉乘的符号判断是算法基石。判断点 P 在有向线段 AB 的左侧还是右侧就看AB × AP的符号。凸包算法Graham 扫描、Andrew 单调链里每一轮判断是左转还是右转用的就是这个。多边形绕序检测、三角形的朝向判断、线段相交判定全都建立在同一件事上。这里也有个实际的经验二维叉乘的符号判断为了避免浮点误差最好带上容差。当|AB × AP|小于某个阈值时说明三点接近共线这时候不要强行判左或判右而是作为共线单独处理。很多凸包实现出现的顶点被吞掉或者多余顶点的 bug根子都在这里——它把浮点噪声当成真实的转向了。写到这里我把自己这些年用叉乘的体会收一收。最有用的一条是任何时候算完叉乘随手做一次垂直性验证。拿结果跟两个输入向量各做一次点乘两个都应该接近零。就这一个习惯帮我挡掉了大量符号错误和下标错误因为它把方向对不对这个抽象问题变成了两个可以直接算出来的数。第二有用的是永远不要在模接近零的时候做归一化判退化用相对阈值而不是绝对零。第三是把坐标系手系写死在项目约定里别让不同模块各自为政。这三点看起来简单但据我观察绝大多数跟叉乘有关的线上事故都能归到这三条里。
返回列表