向量与坐标系
本篇位置:模块一 · 第 1 篇 | 前置:无(系列开篇) | 预计阅读:20 分钟
学习目标
- 会用向量(vector)表示位置、方向、位移,并清楚区分这三者的语义差异。
- 会用点积(dot product)做投影、求夹角、判断朝向。
- 会用叉积(cross product)求法线(normal)与有向面积,理解它在背面剔除中的作用。
- 说清左手系(left-handed system)与右手系(right-handed system)的区别,以及 OpenGL / Vulkan / Android 屏幕坐标三者的坐标系约定差异。
向量是什么:位置、方向、位移
在图形学的语境里,向量(vector)和点(point)长得一模一样——都是一组数字,二维是 (x, y),三维是 (x, y, z)——但它们的几何含义完全不同,这一点值得先讲清楚,因为后面几乎所有的 bug 分类都能追溯到这里的混淆。点描述的是空间中的一个位置,它依赖坐标系原点:把原点挪一下,同一个点的坐标就会变。向量描述的是方向和长度,它不依赖原点——把一个向量在空间里平行搬移到任意位置,它仍然是「同一个」向量。两个点相减会得到一个向量(这个向量表示从起点指向终点的位移,displacement);一个点加上一个向量会得到另一个点。这个「点 - 点 = 向量,点 + 向量 = 点」的代数关系,是后面第 3 篇引入齐次坐标(用第 4 个分量 w 区分点和方向)时要解决的核心问题的起点。
一旦养成「先问这是点还是向量」的习惯,图形管线里大部分变量的角色就变得清晰了。渲染代码里最常见的向量量包括:法线(normal,垂直于表面、决定光照如何反射的方向)、视线方向(view direction,从着色点指向摄像机)、光照方向(light direction,从着色点指向光源,或反过来,取决于约定)、UV 坐标(纹理空间里的二维方向量,虽然常被当作位置使用,但采样偏移、纹理坐标的增量本质是向量运算)、以及运动矢量(motion vector,描述一个像素从上一帧到当前帧在屏幕空间里的位移)。这五类向量会贯穿这个系列的后续文章,尤其是运动矢量——它就是插帧算法的核心输入,本篇末尾的「岗位关联」会展开讲。
基本运算
向量的基本运算并不复杂,但每一条在工程实践里都有对应的坑,值得逐条过一遍。
加减法是分量对分量相加减,几何上对应「首尾相接」:a + b 是先沿 a 走一段,再沿 b 走一段,落点就是结果向量指向的位置;a - b 则是从 b 的终点指向 a 的终点的位移,这也是上一节「两点相减得到位移向量」的运算基础。数乘(标量乘以向量)只改变长度不改变方向(标量为负时方向反转),对应缩放一个位移或者一个速度。
长度(也叫模、norm)定义为
即向量与自身点积再开方——这也是本篇要讲的点积运算的第一个直接应用。三维向量 (x, y, z) 的长度公式展开就是勾股定理的推广:。
归一化(normalize)是把一个向量除以它自身的长度,得到一个长度为 1、方向不变的单位向量:。归一化在图形学代码里几乎无处不在——法线要归一化才能正确参与光照计算,方向向量要归一化才能直接当作 cos 值使用。但这里藏着一个工程上极其常见的 crash 点:如果 v 恰好是零向量,归一化就是除以零。零长度的法线、两个重合点相减得到的零位移向量、退化三角形的边向量,都会触发这个问题——轻则得到 NaN 污染后续所有计算,重则在没有做除零保护的着色器或 C++ 代码里直接产生未定义行为。养成「归一化前判断长度是否接近零」的习惯,是图形代码里少数几个「便宜但容易被忽略」的防御性编程点之一。
点积:图形学里最常用的一个运算
如果说图形学数学里只能选一个运算作为「性价比之王」,点积(dot product)当之无愧。它的定义既可以从分量出发(),也有更有用的几何形式:
其中 是 与 之间的夹角。这个公式同时给了我们三种读法,对应图形学里三个高频用途。
第一种用途是投影。当 是单位向量()时, 直接就是 在 方向上的投影长度——也就是「 在 这条线上的影子有多长」。下图用具体数值演示了这一点:向量 、,先把 归一化得到 ,再用 缩放 ,就得到了 在 方向上的投影向量 ;图中的虚线是从投影点到 终点的垂线,直观展示了「投影」这个词的几何含义——它就是过 的端点向 所在直线作垂线的垂足。
图:点积作为投影。橙色为向量 ,蓝色为向量 ,米色为 在 方向上的投影分量 ;灰色虚线是投影垂线,长度即 。
第二种用途是判断同向还是背向,只需要看点积的符号(不需要真的算出角度或做除法): 说明夹角小于 90°(大致同向),等于 0 说明两者垂直,小于 0 说明夹角大于 90°(大致背向)。这个「只看符号」的技巧在图形管线里出现得非常频繁,例如判断一个表面是不是朝向摄像机(视线方向与法线的点积)、判断两条边是否同侧。
第三种用途是光照强度,也是点积在渲染里最经典的应用:Lambert 漫反射模型认为,一个表面接收到的光照强度正比于表面法线 与指向光源方向 之间夹角的余弦,写成 (两者都取单位向量,负值截断为 0)。这一行代码几乎出现在你能找到的每一个 fragment shader 里——当你在 RenderDoc 里追一个光照计算不对的 bug 时,N·L 通常是第一个要检查的表达式。
叉积:法线与有向面积
点积告诉我们两个向量「有多平行」,叉积(cross product)则告诉我们两个向量「张成的平面有多大、朝哪边」。三维叉积 的结果是一个新向量,它同时垂直于 和 (也就是垂直于 、 所在的平面),长度满足
这个长度恰好等于以 、 为邻边的平行四边形的面积——「叉积模长 = 平行四边形面积(不含符号)」是理解它的最好方式;带符号的版本我们马上在 2D 标量叉积中见到。叉积的方向由右手定则(right-hand rule)确定:右手四指从 的方向弯向 的方向,大拇指指向的就是 的方向(这个规则只在右手系里成立,下一节会讲左右手系的区别)。
叉积在图形学里最直接的用途是求三角形法线。给定三角形三个顶点 (按某个固定绕序,比如逆时针),取两条边向量做叉积就能得到垂直于三角形所在平面的法线:
这个法线的方向由顶点的绕序决定——绕序反了,法线就反向,这也是「模型镜像后光照/背面剔除全错」这类 bug 的根源,第 2 篇讲行列式符号时还会从另一个角度回到这个问题。
在二维场景里,叉积退化成一个标量(可以理解成三维叉积结果向量在 z 轴上的分量):。这个标量的符号可以直接用来判断一个点在一条有向线段的左边还是右边——这是光栅化里判断像素是否落在三角形内部(重心坐标法的基础)、以及背面剔除(判断三角形在屏幕空间的绕序是顺时针还是逆时针)的核心判据。可以说,从「法线怎么算」到「三角形该不该被画出来」,背后都是同一个叉积。
坐标系与手性
向量的加减、点积、叉积,运算规则本身是坐标系无关的,但「右手定则」这类几何规则依赖一个前提:你用的是右手系还是左手系。右手系(right-handed system)的判定方法是:右手食指指向 轴正方向,中指指向 轴正方向,大拇指指向的就应该是 轴正方向;左手系(left-handed system)则反过来,用左手做同样的手势。两套坐标系里,叉积的方向、旋转的正方向、深度值增长的方向都会互为镜像——这正是不同图形 API 之间最容易踩坑的地方之一。
下表整理了这个系列后续会反复用到的三套坐标系约定:
| 坐标系 | 手性 | Y 轴方向 | 深度范围 |
|---|---|---|---|
| OpenGL NDC | 右手系 | +Y 向上 | |
| Vulkan NDC | Y 轴翻转导致整体呈左手约定 | +Y 向下 | |
| Android 屏幕坐标 | 二维像素坐标,无手性概念 | 原点在左上角,+Y 向下 | 不适用 |
这张表看着只是几个符号差异,但它是后面 M3b 模块「Vulkan viewport 翻转」问题的伏笔:同一段场景数据,如果不做任何处理直接把 OpenGL 管线的输出接到 Vulkan 管线上,画面会上下颠倒——原因就是 NDC 里 Y 轴方向反了;而 Android 的窗口系统(Surface、SurfaceFlinger 的图层坐标)用的是屏幕坐标那一套「原点左上、Y 向下」的约定,这和 OpenGL 的 NDC 又不是一回事。这三套约定在这个系列的中段会正面遭遇,这里先把差异摆出来,让读者对「同一个点,坐标可能不一样」这件事有心理预期。
🎯 岗位关联
运动矢量(motion vector)本质上就是一个屏幕空间的位移向量:它描述某个像素内容从上一帧的屏幕位置移动到当前帧屏幕位置的位移,是插帧(frame interpolation)算法最核心的输入——插帧算法要做的事,粗略讲就是沿着运动矢量在时间轴上做插值(这也是第 6 篇的主题)。法线与视线(或光源方向)的点积,是读懂任意一段 shader 代码时最先要认出来的表达式,做渲染分析、排查光照类问题时它是绕不开的基础词汇。而坐标系手性的差异——尤其是 Y 轴方向和深度范围——是接手 GLES 到 Vulkan 移植类工作时第一个会踩的坑:一个视口翻转错误往往只在特定分辨率或特定后处理阶段才会暴露出来,排查起来比想象中费时间。
小结
| 运算 | 几何意义 | 渲染中的典型用途 |
|---|---|---|
| 加减法 | 位移的合成/分解 | 位置更新、相对位移计算 |
| 数乘 / 归一化 | 缩放长度 / 保留方向取单位长度 | 单位方向向量、速度缩放 |
| 点积 | 投影、夹角余弦、同向性 | 光照强度 、朝向判断 |
| 叉积 | 垂直向量、有向面积 | 三角形法线、背面剔除、点在线段哪侧 |
| 坐标系手性 | 基向量的相对朝向 | OpenGL/Vulkan NDC 与 Android 屏幕坐标换算 |
延伸阅读
- 3Blue1Brown《线性代数的本质》:https://www.bilibili.com/video/BV1ys411472E
- GAMES101 Lecture 2:https://games-cn.org/intro-graphics/
- Scratchapixel: Points, Vectors and Normals:https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/geometry/points-vectors-and-normals.html
