Android系统工程师够用的图形学数学基础
Android系统工程师够用的图形学数学基础
本篇位置:模块一 · 开篇 | 前置:无 | 预计阅读:10 分钟
这是一个系列教程的开篇。它写给每天在 libgui / libhwui / SurfaceFlinger / libEGL / libvulkan 里打转,API 都会用,但数学基本忘光了的人。
标题里"够用"两个字是认真的:不追求把图形学数学讲完整,只讲读 AOSP 显示相关代码会撞上的那些。
一、为什么要学:这些代码,不补数学就读不动
在 AOSP 显示这条线上,数学不是"背景知识",是直接写在代码里的。下面五段都是你 review 时会遇到、迟早要动的地方。
1. Transform::set():三个变换是怎么"乘"到一起的
frameworks/native/libs/ui/Transform.cpp,处理图层的翻转和旋转:
status_t Transform::set(uint32_t flags, float w, float h) {
Transform H, V, R;
if (flags & FLIP_H) {
mat33& M(H.mMatrix);
M[0][0] = -1; M[2][0] = w;
}
if (flags & FLIP_V) {
mat33& M(V.mMatrix);
M[1][1] = -1; M[2][1] = h;
}
if (flags & ROT_90) {
const float original_w = h;
mat33& M(R.mMatrix);
M[0][0] = 0; M[1][0] = -1; M[2][0] = original_w;
M[0][1] = 1; M[1][1] = 0;
}
*this = (R*(H*V)); // ← 三个变换被"乘"到了一起
return NO_ERROR;
}它在干什么:把 FLIP_H / FLIP_V / ROT_90 三个标志各自兑现成一张矩阵,再合成一张。
不补数学,这段你答不上来的问题:
- 为什么是
R*(H*V),不是(H*V)*R?调换会怎样? M[0][0] = -1是翻转,那紧跟着的M[2][0] = w是干什么的?为什么翻转要配一个w?- 明明只处理二维,为什么是
mat33而不是mat22?
这三个问题背后分别是:矩阵乘法不满足交换律、齐次坐标的平移列、平移不是线性变换。不知道这些,这段代码你只能照抄,不敢动——而排查旋转/镜像相关的显示问题,动的就是这里。
2. Transform::asMatrix4():十六行赋值,每一行都在搬位置
mat4 Transform::asMatrix4() const {
mat4 m = mat4{mat4::NO_INIT};
m[0][0] = mMatrix[0][0]; // 左上 2x2 原样搬过去
m[0][1] = mMatrix[0][1];
m[0][2] = 0.f;
m[0][3] = mMatrix[0][2]; // 原来的第三行 → 新的第四行
// ...
m[2][2] = 1.f; // 新插进来的第三维:什么都不做
m[3][0] = mMatrix[2][0]; // 原来的第三列(平移)→ 新的第四列
return m;
}它在干什么:把二维用的 3×3 塞进 3D 管线要的 4×4。
不补数学的话:这十六行就是一串看不出规律的下标搬运。你不知道为什么 m[3] 是平移列(因为 mat4 是列主序)、为什么新插进来的第三维要填单位、为什么原来的"第三行"要变成"第四行"。哪天要往里加一个 z 方向的效果,你连从哪下手都不知道。
3. ColorSpace::sRGB():十三个魔法数字
frameworks/native/libs/ui/ColorSpace.cpp:
const ColorSpace ColorSpace::sRGB() {
return {
"sRGB IEC61966-2.1",
{{float2{0.640f, 0.330f}, {0.300f, 0.600f}, {0.150f, 0.060f}}},
{0.3127f, 0.3290f},
{2.4f, 1 / 1.055f, 0.055f / 1.055f, 1 / 12.92f, 0.04045f, 0.0f, 0.0f}
};
}它在干什么:前三对是 CIE 1931 色度图上红绿蓝三个原色的 坐标——也就是这套标准能表达的那个三角形的三个顶点;第四对是白点;最后七个是分段传递函数的参数,喂给这个函数:
static constexpr float response(float x, const ColorSpace::TransferParameters& p) {
return x >= p.d ? std::pow(p.a * x + p.b, p.g) : p.c * x;
}不补数学的话:这十三个数就是十三个不能碰的常量。而这活儿是会落到你头上的——厂商屏幕有自己的原生色域,要往系统里加一个新的 ColorSpace,你得知道这些数去哪查、怎么填、为什么那条曲线要分成两段、分段点凭什么在 0.04045。
4. computeXYZMatrix():你天天在用的那三个数是它算出来的
// libs/nativewindow/include/android/data_space.h
// Use the unadjusted KR = 0.2126, KB = 0.0722 luminance interpretation0.2126 / 0.7152 / 0.0722——算亮度、转灰度、做 YUV,到处都是它们。很多人当成"业界约定俗成的三个数"抄了一辈子。
它们不是约定,是算出来的:ColorSpace::computeXYZMatrix(primaries, whitePoint) 从上面那三个原色坐标和白点解出 RGB→XYZ 的 3×3 矩阵,那个矩阵的第二行就是这三个数。换一个色域(比如 P3),这三个数就变了。
不补数学的话:你不知道它们会变,于是在 P3 内容上套 BT.709 的系数,算出来的亮度是错的——而且不报错。
5. RenderProperties::updateMatrix():为什么一旦绕 X/Y 转就要动用相机
frameworks/base/libs/hwui/RenderProperties.cpp:
if (MathUtils::isZero(getRotationX()) && MathUtils::isZero(getRotationY())) {
// 纯二维:平移、绕 Z 转、缩放,全在一个 SkMatrix 里搞定
transform->setTranslate(...);
transform->preRotate(...);
} else {
// 一旦要绕 X 或 Y 转,就得动用一台"相机"
mComputedFields.mTransformCamera.rotateX(...);
mComputedFields.mTransformCamera.rotateY(...);
}它在干什么:View 的 rotationX / rotationY 一旦非零,hwui 就走另一条路,凭空多出一台相机。
不补数学的话:你不知道这个分支为什么存在,也就不知道相机被放在屏幕后面 8 英寸()是什么意思、改这个数会让动画看起来怎样。卡片翻转动画透视过头了要调,你只能试数。
看懂、改动、改对
这五段有个共同点:代码本身没有难度,难的是每个数在数学上代表什么。
- 看懂:知道
M[2][0] = w那个w是平移量,不是什么神秘偏移 - 改动:敢动
R*(H*V)的顺序,因为你知道动了会发生什么 - 改对:知道 0.2126 会随色域变、知道 4×4 是列主序、知道平移为什么必须靠第三维
差别不在能不能跑起来,在于出了问题你是在推导还是在试数。这类 bug 通常不崩溃、不报错,只是画面"说不上哪儿不对"——试数是试不出来的。
这也是这套教程的基本思路:不从零建立直觉,而是给你已有的工程直觉配上数学名字。 你早就知道"变换能叠加、顺序换了结果不一样"——缺的只是有人告诉你,这件事叫矩阵乘法,它不满足交换律。名字是钥匙,有了名字才能去查、去推、去判断一段陌生代码有没有踩坑。
二、这个教程讲了什么
写法:每一篇都从一段你读过的 AOSP 源码开始,抛出几个"你可能从没细想过"的问题,正文推完,结尾再回到那段代码把问题逐个答掉。
主线:从你最熟的 2D 图层合成出发,一路推到最陌生的 3D 渲染,每一步都从已知推到未知。
篇目
| # | 篇目 | 讲什么 | 时长 |
|---|---|---|---|
| 附 | 数学速查表 | 弧度、sin/cos、函数图像、常用记号。不必顺读,卡住了再翻 | — |
| Part 1 · 从你熟的 2D 说起(读得快一些,这些你天天在用) | |||
| 1 | 坐标系与向量 | Rect 为什么 top < bottom;四个 ProjectionSpace 为什么存在;点积的三种用法 | 35 min |
| 2 | 2D 变换与矩阵 | 光看矩阵两列就读出它把空间怎么了;缩放/翻转/旋转;组合顺序、行列式、逆与转置 | 40 min |
| 3 | 齐次坐标:为什么是 3×3 不是 2×2 | ui::Transform::set 怎么把平移也塞进矩阵;SkMatrix 的 kMPersp0/1/2 与透视除法 | 35 min |
| Part 2 · 升到 3D(要放慢,这块对你基本是新的) | |||
| 4 | 3D 变换与 MVP 链 | hwui 的 rotationX 为什么要动用一台相机;含小节「四元数是干吗的」 | 45 min |
| 5 | 投影与深度 | currentTransform 该拿去干什么;24 位深度为什么还会 z-fighting | 45 min |
| Part 3 · 像素级的数学(最值得反复读,你的半懂地带) | |||
| 6 | 插值 | needsFiltering() → kLinear 这条链;双线性放大为什么发糊;插值和滤波是一回事 | 40 min |
| 7 | 采样、走样与滤波 | kInputScale = 0.25f 为什么不算 bug;奈奎斯特、卷积、mipmap 为什么存在 | 45 min |
| 8 | Gamma 与线性空间 | response() 为什么分段;OETF 和 EOTF 哪个是哪个;线性空间铁律 | 40 min |
| 9 | 色域、YUV 与 HDR | Dataspace 三段位域各管什么;0.2126/0.7152/0.0722 从哪来 | 50 min |
正文九篇合计约 6 小时。别指望一口气读完,按 Part 分批更合适。
每篇末尾有自测,答案是折叠的——先自己答一遍再展开。
提前打个招呼:两个坐标约定
系列里换过一次坐标系,这不是笔误:
| 用在哪 | 约定 | |
|---|---|---|
| 屏幕/图像坐标 | Part 1 与 Part 3(第 1~3、6~9 篇) | 原点左上, 右, 下 |
| 图形学右手系 | Part 2(第 4~5 篇) | 上,相机看向 |
合成器一套、渲染管线一套,真实世界里就是并存的。与其假装它们一样,不如在切换处说清楚——第 4 篇开头有专门提示。
源码基准与插图
正文引用的 AOSP 代码摘自 Android 17 分支,贴精简片段 + 文件路径,不带行号(行号必随版本漂移)。不同版本可能略有出入,但类名、字段名和整体结构是稳定的。
插图全部由脚本生成,不是手绘——这样能保证图里的每个数字都和正文的算例同源。手画的示意图很容易和正文的具体数值对不上,读者照着推就会卡住。
从哪开始读
按顺序的话,从第 1 篇 · 坐标系与向量开始。
只想解决某个具体问题,直接跳到上面表里对应的那一篇——每篇开头都标了前置依赖,缺哪补哪。
系列入口:http://ahaoframework.tech/01-math/

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