坐标系与向量
坐标系与向量
1. 坐标不是点的属性
一个像素在屏幕上就待在那儿,它自己不知道自己的坐标是多少。
坐标是一个答案,而问题是:"从某个原点出发,沿着某几根轴,各走多远?" 换一个原点或换一组轴,同一个像素的答案就变了。
拿屏幕旋转 180° 举例:一部 1080×1920 的手机,屏幕转了半圈之后,图层自己坐标系里的点 ,在屏幕坐标系里变成了 。
图:同一个像素,两套数字。左边是图层自己的坐标系,右边是屏幕转 180° 之后的坐标系。像素没动,动的是"从哪儿开始数"。
于是有了本篇第一条结论:
一个坐标值单独拿出来是没有意义的,它必须带着"我属于哪个空间"这条信息。
真实的程序里,尺子通常不止两把:画图的代码用一套,屏幕用一套,中间每转一次、缩放过一次,还会多出一套。所以代码里常给每个坐标系起一个名字,强迫每个坐标值都写明它是用哪把尺子量的。
2. 屏幕坐标的 Y 轴常常向下
数学课本画坐标系,原点在左下角, 朝上。Android 屏幕坐标反过来:原点在左上角, 朝下。
图:同一组数字 ,在两套约定里落在画面的不同位置。
这个约定的来源很朴素——扫描线是从屏幕顶端往下扫的,从左上角开始数最自然。
3. 点、向量、位移:长得一样,不是一回事
可能是两种完全不同的东西:
- 点(point):一个位置。"这个像素在 、 处。"它依赖原点——挪动原点,数字就变。
- 向量(vector):一个方向加长度。"往右 100、往下 200。"它不依赖原点——把它平移到画面任何地方,它还是同一个向量。
图: 和 是两个点(有确定位置)。橙色箭头 是一个向量,它描述"从 A 到 B 要怎么走"。把这个箭头原样搬到画面左下角(蓝色),它仍然是同一个向量——因为向量只关心方向和长度。
由此得到一组必须记住的规则:
| 运算 | 结果 | 有没有意义 |
|---|---|---|
| 点 − 点 | 向量 | ✅ 从后者指向前者的位移 |
| 点 + 向量 | 点 | ✅ 从这个点出发挪一段 |
| 向量 + 向量 | 向量 | ✅ 两段位移接起来 |
| 向量 × 数 | 向量 | ✅ 方向不变,长度缩放 |
| 点 + 点 | — | ❌没有意义 |
4. 向量的基本运算
加减是分量各自加减,几何上是"首尾相接":
数乘只改长度不改方向(乘负数会掉头):
长度(也叫模,写作 )就是勾股定理:
归一化(normalize)是除以自己的长度,得到一个方向不变、长度正好为 1 的向量,记作 :
验算一下: ✓
归一化有一个必须防的坑:零向量除以零。
长度为 0 的向量归一化会得到
0/0,结果是NaN。而NaN有个恶劣性质——它参与的任何运算结果还是NaN,会顺着计算链一路污染下去,最后你在很远的地方看到一片黑或者一片乱码,回溯起来极其费劲。零向量是怎么来的?两个重合的点相减、退化成一条线的三角形、被算成 0 的缩放系数。归一化之前先判长度是不是接近 0,是少数几个"极便宜但极容易漏"的防御点。
5. 点积:一个乘法,三种用法
点积(dot product)把两个向量变成一个数。按分量算:
它还有一个更有用的等价形式:
其中 是两个向量的夹角。这两个式子说的是同一件事,但第二个式子解释了它为什么有用。
用法一:求投影长度
当 是单位向量时(),上面的公式退化成 ——这正是 投在 方向上的影子长度。
图:,(屏幕坐标里 表示朝上)。先把 归一化, 就是米色那一段的长度。灰色虚线是从 的端点向 作的垂线。
"把一个向量拆成沿某方向的分量 + 垂直于该方向的分量"是图形学里的高频动作,靠的就是这一条。
用法二:判断朝向(只看符号,不用算角度)
当 、 都不是零向量时, 和 都为正,点积的符号完全由 决定:
| 点积 | 含义 | |
|---|---|---|
| 小于 90° | 大致同向 | |
| 正好 90° | 互相垂直(前提:两者都非零) | |
| 大于 90° | 大致反向 |
这个判断不需要开方、不需要反三角函数,只要几次乘加。"这个面朝不朝着摄像机""这两个方向是不是差不多""这两条边垂不垂直",代码里全用它。若任一输入可能是零向量,要先单独处理,因为零向量没有方向,也谈不上夹角。
算例:,
负数,所以两者夹角大于 90°,大致反向。
用法三:光照强度
这个用途要等到后面讲法线才能完整展开,这里先混个眼熟:几乎每个 fragment shader 里都有一行形如 max(dot(N, L), 0.0) 的代码, 是表面法线、 是指向光源的方向,两者都归一化过。它算的是"这个面正对着光的程度"——正对时点积接近 1(最亮),侧着时接近 0(最暗),背着光时为负数、被 max 截断成 0。
看 shader 代码时,dot ** 是出现频率最高的函数之一** ,认出它在算什么,能省掉一大半阅读时间。
6. 二维叉积:判左右、判绕序、算面积
三维的叉积留到第 4 篇(那时候才需要求法线)。二维平面上有一个更简单的版本,它的结果是一个数而不是向量:
它同样有两层含义。
符号:b 在 a 的哪一边
沿着 的方向走, 是偏向你的左手边还是右手边,看这个数的符号就知道。
由此可以判断一个点落在一条有向线段的哪一侧:
图左:线段从 指向 。对任意一点 ,算 ,负号表示 在 前进方向的左手边,正号表示右手边。图右:沿着 走一圈,叉积的符号说明这个三角形的绕序。
具体算一个。,,:
负号,所以 在左手边。
这里又是 轴朝向的坑。 在数学课本的画法( 向上)里,正的叉积对应"逆时针";在屏幕坐标( 向下)里,同一个公式的正号对应画面上的顺时针。
公式本身没变,变的是你看画面的方式。所以任何一份关于"顺时针还是逆时针"的约定,都必须同时说清它用的是哪套坐标系,否则那句话就是没有信息量的。
绝对值:平行四边形的面积
和点积类似,叉积也有一个几何公式:
其中 是两个向量的夹角。对比点积的 ,叉积用的是 ——这正好对应"垂直分量"而不是"平行分量"。
而 恰好等于以 、 为两条邻边的平行四边形面积:底边长度 ,高度是 在垂直于 方向上的投影 。除以 2 就是三角形的面积。
上图右边的三角形:,,
面积 。
应用:判断点是否在三角形内
这两条含义合起来,正是"逐像素填色"的数学地基。现在详细说明怎么用叉积判断一个点(比如一个像素中心)在不在三角形内部。
核心思路:对三角形的三条边各算一次叉积,看三个符号是否一致。
回到上面图右的三角形 (按某个固定绕序)。对于任意一点 ,我们要检查它在不在三角形内部:
- 第一条边 :算 ,判断 在这条边的哪一侧
- 第二条边 :算 ,判断 在这条边的哪一侧
- 第三条边 :算 ,判断 在这条边的哪一侧
如果三角形是按顺时针绕的(在屏幕坐标下),那么一个在内部的点,对三条边算出的叉积都应该是正数(都在边的"右手边");如果三角形是逆时针绕的,那三个叉积都应该是负数。只要有一个符号和其他两个不同,点就在三角形外面。
具体算一个。用上面的三角形:,,。测试点 :
边 1:
边 2:
边 3:
三个都是正数,所以 在三角形内部。这就是 GPU 光栅化器每秒做几十亿次的计算——每个像素中心都要对覆盖它的三角形做这三次叉积判断。
而这三个叉积值再除以总面积,就是第 6 篇要讲的重心坐标——顶点的颜色、纹理坐标怎么填满三角形内部,全靠它。背面剔除(判断一个三角形是正面还是反面朝向观察者)同样是看叉积的符号。把三角形逐个像素填出来这件事是模块二的主题,本篇只把数学地基打好。
自测
问题 1:同一个点为什么在不同坐标系里有不同的数?
因为坐标是"从某个原点出发,沿着某几根轴各走多远"这个问题的答案,换一个原点或换一组轴,答案就变。像素自己一动没动—— 和 是同一个像素在两套尺子下的两个答案。所以代码里常给每个坐标系起名字,强迫每个坐标值带上"我属于哪个空间";两套数字之间的换算规则,第 2 篇会展开。
问题 2:屏幕坐标的 轴为什么常常向下?
因为扫描线从屏幕顶端往下扫,从左上角开始数最自然。直接后果:画面上"上面"的边 值更小,矩形区域的高度是下边减上边。 轴方向一翻转,左右、绕序这类判断的符号会跟着翻,本篇第 6 节和第 2 篇第 3 节各遇到一处。
问题 3:点和向量都是几个数,为什么不能混着用?
点是位置,依赖原点;向量是方向加长度,不依赖原点。位置减位置得到位移(合法),位置加位移得到新位置(合法),但位置加位置不对应任何几何量——它会随着原点挪动而变化,是个没有含义的数。
以后再看到一个坐标值,你应该先问的不是"它是多少",而是"它属于哪个空间、它是位置还是位移"。 图形代码里绝大多数"位置偏了一点点"或者"整个歪掉"的 bug,追到底都是这两个问题里的一个答错了。
1. 一个矩形区域用四条边描述:left=100, top=200, right=100, bottom=500。这个矩形是"空"的(盖不住任何面积)吗?为什么?
是空的。
宽度 = right − left = 100 − 100 = 0,满足"宽 ",所以是空矩形。它高 300 但宽 0,是一条竖线,盖不住任何像素。
2. 向量 (−3, 4) 的长度是多少?归一化之后是什么?
归一化:。验算 ✓
注意长度永远非负,负分量平方之后就没有负号了。
3. 已知 a·b = 0 且两者都不是零向量,能得出什么结论?
两者互相垂直。
因为 ,两个长度都不为 0,那只能是 ,即 。
补充一句:如果允许零向量,点积为 0 也可能是因为其中一个是零向量——所以判垂直之前要先排除这种情况,这和第 4 节那个归一化的坑是同一类问题。
4. 三角形三个顶点 (0,0)、(4,0)、(0,3),用叉积算它的面积。
,
面积 。
对照直角三角形公式 ✓
5. 同事说"我们约定三角形按逆时针绕序算正面"。这句话缺了什么信息?
缺了用哪套坐标系看。
"逆时针"是一个关于画面的描述,而画面朝向取决于 轴指哪边。同一组顶点数据,在 向上的坐标系里是逆时针,在 向下的屏幕坐标里看就是顺时针。
所以完整的说法必须是"在某某坐标系下按逆时针"。不同图形 API 的默认正面约定、以及画面上下颠倒之后约定要不要跟着翻,踩的都是这个坑。
小结
| 概念 | 一句话 | 怎么记 |
|---|---|---|
| 坐标是相对的 | 同一个像素在不同空间里数字不同 | 同一个像素两套数: 与 |
| 屏幕 轴 | 原点左上, 向下 | "上面"的边 值更小,高度 = 下边减上边 |
| 点 vs 向量 | 点是位置,向量是方向+长度 | 同样几个数,一个答"在哪儿",一个答"往哪走" |
| 合法运算 | 点−点=向量,点+向量=点;点+点没意义 | 宽度 = 右边减左边,是位置减位置 |
| 长度与归一化 | ;除以长度得单位向量 | 归一化前必须防零向量,否则NaN 扩散 |
| 点积 | ;投影、判垂直、判同向 | shader 里的dot(N, L) |
| 点积符号 | 正=同向,零=垂直,负=反向;不用算角度 | 朝向判断的标准写法 |
| 二维叉积 | ;符号管左右绕序,绝对值管面积 | 判像素在不在三角形内、背面剔除 |
| 绕序的陷阱 | "顺/逆时针"必须说清是哪套坐标系 | 轴一翻,结论就反 |
延伸阅读
- 3Blue1Brown《线性代数的本质》第 1、7、8 集(向量、点积、叉积):https://www.bilibili.com/video/BV1ys411472E
- GAMES101 Lecture 2:Review of Linear Algebra: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

觉得有用?关注公众号「阿豪讲Framework」
Android 系统开发,新文章第一时间推送。