几何与变换
Shape
概念与动机
在 GPU 渲染管线中,矢量路径(Path)的光栅化处理涉及高昂的计算开销,通常需要将贝塞尔曲线进行线段细分、多边形三角化,或离屏光栅化为蒙版纹理。在帧渲染循环中重复计算静态路径,会导致显著的 CPU 与 GPU 性能瓶颈。
Shape 机制即为解决此性能隐患而引入。作为一种不可变的矢量图形对象,Shape 在首次计算完成后,其底层生成的顶点数据或蒙版纹理会被 GPU 缓存。后续渲染同一 Shape 实例时,渲染引擎将直接命中缓存并提交绘制,从而规避重复的几何拓扑计算开销。相比之下,Path 更适用于单次绘制或需要高频突变拓扑结构的动态场景。两者核心特性对比如下:
| 维度 | Path | Shape |
|---|---|---|
| 可变性 | 可变,允许动态修改拓扑 | 不可变,初始化完成后状态锁定 |
| 线程安全 | 否,缺乏并发控制 | 是,支持安全的跨线程共享与并行构建 |
| GPU 缓存 | 每次 drawPath 均触发重新计算 | 计算结果隐式缓存,复用绘制开销趋近于零 |
关键 API 协作流程
Shape 对象的构建与渲染流程如下:
// 1. 构建 Shape 对象
auto shape = Shape::MakeFrom(path); // 从 Path 转换
auto shape = Shape::MakeFrom(textBlob); // 从 TextBlob 转换
auto shape = Shape::MakeFrom(pathProvider); // 从 PathProvider 转换
auto shape = Shape::MakeFrom(font, glyphID); // 从 Font Glyph 转换
// 2. 应用变换或效果 (可选)
shape = Shape::ApplyStroke(shape, &stroke);
shape = Shape::ApplyMatrix(shape, matrix);
shape = Shape::ApplyMatrix3D(shape, matrix3D);
shape = Shape::ApplyEffect(shape, effect);
shape = Shape::Merge(shape1, shape2, pathOp);
// 3. 提交绘制
canvas->drawShape(shape, paint);
注意:所有
Apply*与Merge静态变换方法均采用非破坏性操作,返回全新的包装对象,原始Shape内存与状态保持不变。
代码示例
示例 1:基础图形构建与绘制
void draw(Canvas* canvas) {
auto path = Path();
path.addRoundRect(Rect::MakeXYWH(0, 0, 200, 100), 15, 15);
auto shape = Shape::MakeFrom(std::move(path));
Paint paint;
paint.setColor(Color::Blue());
canvas->drawShape(shape, paint);
}

示例 2:几何拓扑合并
Shape 提供两种图形合并策略:
- 双源合并:通过
Shape::Merge(shape1, shape2, pathOp)对两个源图形执行合并操作。PathOp枚举决定合并策略:Append和Extend直接追加几何数据,不执行空间布尔运算;Union、Intersect、Difference、XOR则对两者执行几何布尔运算,生成运算后的新轮廓。 - 批量合并:通过
Shape::Merge(shapes)将多个Shape实例的几何数据一次性合并为一个复合图形。
void draw(Canvas* canvas) {
Path rectPath;
rectPath.addRect(Rect::MakeXYWH(0, 0, 80, 80));
auto rectShape = Shape::MakeFrom(std::move(rectPath));
Path ovalPath;
ovalPath.addOval(Rect::MakeXYWH(40, 10, 80, 60));
auto ovalShape = Shape::MakeFrom(std::move(ovalPath));
Paint paint;
paint.setColor(Color::Blue());
// 执行布尔运算并提交渲染
canvas->drawShape(Shape::Merge(rectShape, ovalShape, PathOp::Append), paint);
canvas->drawShape(Shape::Merge(rectShape, ovalShape, PathOp::Extend), paint);
canvas->drawShape(Shape::Merge(rectShape, ovalShape, PathOp::Union), paint);
canvas->drawShape(Shape::Merge(rectShape, ovalShape, PathOp::Intersect), paint);
canvas->drawShape(Shape::Merge(rectShape, ovalShape, PathOp::Difference), paint);
canvas->drawShape(Shape::Merge(rectShape, ovalShape, PathOp::XOR), paint);
}

示例 3:多重变换的链式调用
Shape 的 Apply* 系列方法允许流式构建复杂的图形特效:
void draw(Canvas* canvas) {
Path path;
path.addRoundRect(Rect::MakeXYWH(0, 0, 100, 100), 15, 15);
auto shape = Shape::MakeFrom(std::move(path));
auto dashEffect = PathEffect::MakeDash({10.f, 5.f}, 2, 0.f);
shape = Shape::ApplyEffect(shape, dashEffect);
auto stroke = Stroke(10.0f);
shape = Shape::ApplyStroke(shape, &stroke);
shape = Shape::ApplyMatrix(shape, Matrix::MakeScale(1.5, 0.5));
Paint paint;
paint.setColor(Color::Blue());
canvas->drawShape(shape, paint);
}

描边策略
TGFX 渲染管线提供了两种截然不同的矢量描边方式。这两种方式在底层计算时机与 GPU 缓存复用机制上存在根本性差异,开发者需根据业务场景的动态特征进行合理选型:
- 静态描边(Shape 级):通过提前调用
Shape::ApplyStroke,将描边属性固化并保存为一个独立持久的Shape实例。由于该实例具有固定的UniqueID,底层的描边几何计算只需执行一次,生成的图元或蒙版数据会被 GPU 稳定缓存。适用于线宽、颜色固定且高频复用的 UI 图形。
auto strokedShape = Shape::ApplyStroke(shape, &stroke);
canvas->drawShape(strokedShape, paint);
- 动态描边(Paint 级):通过
paint.setStyle(PaintStyle::Stroke)配置描边。每次调用drawShape时,渲染引擎会在内部临时创建一个具备全新UniqueID的描边包装对象,从而强制 CPU 逐帧重新执行描边拓扑计算并刷新显存数据。因此,该模式专用于描边参数(如线宽等)存在持续插值动画更新的场景。
paint.setStyle(PaintStyle::Stroke);
paint.setStroke(stroke);
canvas->drawShape(shape, paint);
注意:无论是"静态描边"还是"动态描边",描边最终都会转化为具备物理宽度的几何图元(如多边形面片或覆盖率蒙版)参与渲染。因此,这两种描边的物理线宽都会受 Canvas 全局缩放矩阵(Scale Matrix)的影响而发生缩放形变。
注意事项
- 首次渲染延迟:
Shape的几何解析与 GPU 资源分配采用延迟初始化策略。首次调用drawShape时会触发复杂的几何解析、多边形三角化或离屏蒙版光栅化等计算,可能引起首帧耗时毛刺。对于性能高度敏感场景,建议预调用getPath()强制将部分解析负载前置。 - 空间变换的非交换律:变换算子的调用链路将直接决定最终空间状态。先调用
ApplyStroke再调用ApplyMatrix(Scale)会导致物理线宽随基准坐标系一同缩放;若逆序调用,则线宽不受该矩阵缩放的影响。 - 变换矩阵对 GPU 缓存的影响:若
Shape::ApplyMatrix传入的矩阵仅包含平移分量,引擎会直接复用原几何体的 GPU 缓存。但若引入了缩放、旋转或透视等复杂变换,原有的缓存将无法复用,底层必须重新进行几何解析或离屏蒙版光栅化,并将新生成的数据(顶点或纹理)上传至 GPU,可能导致额外的性能开销。
Mesh
概念与动机
尽管 Shape 和 Path 极大简化了基于高阶曲线的矢量轮廓表达,但其内部的三角剖分算法对用户呈现黑盒状态,开发者无法干预单个几何顶点的物理状态。为此,TGFX 提供了底层图形抽象接口 Mesh,以满足复杂曲面变形计算、海量实例化的粒子发射器、非线性纹理坐标映射等高度定制化的管线需求。
Mesh 是框架层暴露的顶点级绘制 API。允许开发者直接提供顶点坐标、纹理映射坐标(UV)及逐顶点色值等数据。该对象同样遵循不可变与线程安全范式,引擎内部会自动管理其 GPU 缓存。
关键 API 协作流程
// 1. 构建 Mesh 对象
auto mesh = Mesh::MakeCopy(topology, count, positions, texCoords, colors); // 直接传入顶点数据
auto mesh = Mesh::MakeFromPath(path); // 从 Path 转换
auto mesh = Mesh::MakeFromShape(shape); // 从 Shape 转换
// 2. 提交绘制
canvas->drawMesh(mesh, paint);
代码示例
下述代码演示了如何构建自定义网格数据,模拟旗帜飘动的非线性形变效果:
void draw(Canvas* canvas) {
auto image = Image::MakeFromFile("flag.jpg");
auto imageWidth = static_cast<float>(image->width());
auto imageHeight = static_cast<float>(image->height());
// 12×6 网格(13 列 7 行 = 91 个顶点),模拟风力场扰动
const int cols = 13, rows = 7;
const float cellW = 160.f / (cols - 1), cellH = 80.f / (rows - 1);
Point positions[cols * rows];
Point texCoords[cols * rows];
Color colors[cols * rows];
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
auto i = r * cols + c;
// 左边缘锚定(t=0 时偏移恒定),随距离逐渐增强波振幅
auto t = static_cast<float>(c) / (cols - 1);
auto s = static_cast<float>(r) / (rows - 1);
positions[i] = {20.f + c * cellW + sinf(s * 4.5f) * t * 20.f,
20.f + r * cellH + cosf(t * 3.6f) * t * 12.f};
// 注意:纹理坐标系采用绝对像素空间,而非 [0, 1] 归一化空间
texCoords[i] = {c * imageWidth / (cols - 1), r * imageHeight / (rows - 1)};
colors[i] = Color::White();
}
}
// 构建独立三角形索引缓冲区
uint16_t indices[(cols - 1) * (rows - 1) * 6];
int idx = 0;
for (int r = 0; r < rows - 1; r++) {
for (int c = 0; c < cols - 1; c++) {
auto tl = static_cast<uint16_t>(r * cols + c);
auto tr = static_cast<uint16_t>(tl + 1);
auto bl = static_cast<uint16_t>(tl + cols);
auto br = static_cast<uint16_t>(bl + 1);
indices[idx++] = tl; indices[idx++] = tr; indices[idx++] = bl;
indices[idx++] = tr; indices[idx++] = br; indices[idx++] = bl;
}
}
auto mesh = Mesh::MakeCopy(MeshTopology::Triangles, cols * rows, positions, texCoords,
colors, idx, indices);
Paint paint;
paint.setShader(Shader::MakeImageShader(image));
canvas->drawMesh(mesh, paint);
}

拓扑模式
枚举类 MeshTopology 决定了渲染管线如何将顶点数据组装为基本图元:
- Triangles(独立三角形):每 3 个连续的顶点(或索引)明确构成一个独立的三角形。如图所示,8 个顶点通过 12 个索引组合成
(v0,v1,v2)等 4 个独立面片。此模式提供最大的几何灵活性,适用于非连通或无序的离散模型。 - TriangleStrip(三角带):从第 3 个顶点起,每个新增顶点都会与它紧邻的前两个顶点隐式构成一个新的三角形,即依次生成
(v0,v1,v2)、(v1,v2,v3)、(v2,v3,v4)、(v3,v4,v5)等面片。如图所示,仅 6 个顶点即可生成 4 个相连的三角形。该模式避免了冗余的顶点或索引传递,非常适合连通的带状图形或矩形阵列,可大幅减少内存占用与传输带宽。

注意事项
- 16 位索引缓冲区限制:
Mesh内部绑定的顶点索引类型限定为uint16_t。因此,单个绘制调用的网格图元最大顶点数严格受限于 65,536 个。应对超大模型资产时,必须实施业务侧的几何分块与多批次渲染。 - 颜色混合规则:渲染
Mesh时,最终的像素颜色由 Mesh 的顶点数据(颜色、UV)与Paint的属性(基础色、Shader)共同决定。其计算逻辑严格遵循以下层次:- 1. Shader 坐标映射:若
Paint挂载了任何类型的 Shader(包括图片纹理、线性/径向渐变等),该 Shader 会优先使用 Mesh 提供的 UV 坐标 进行采样或计算(这使得渐变色等特效也能像常规纹理贴图一样,随着网格的扭曲或拉伸自然变形)。若 Mesh 未提供 UV,底层会自动回退,使用顶点的局部物理坐标(Position)作为 Shader 的计算依据。 - 2. 颜色混合(Modulate):若 Mesh 带有顶点颜色,且
Paint挂载了 Shader,最终像素颜色为 顶点颜色 × Shader 计算颜色。若两者仅存其一,则直接输出存在的那个颜色。 - 3. 基础色兜底:
Paint设置的基础颜色(setColor)不参与前两者的混合。当 Mesh 带有顶点颜色或Paint挂载了 Shader 时,基础颜色的设置将被直接忽略。只有当 Mesh 既无顶点颜色,Paint也未挂载 Shader 时,才会直接输出该基础色。
- 1. Shader 坐标映射:若
- 非归一化纹理坐标:TGFX 不使用传统的
[0.0, 1.0]归一化 UV 空间。Mesh 的纹理坐标必须直接使用纹理真实的像素坐标(例如,若纹理尺寸为 200×100,则右下角的 UV 坐标应设为200, 100)。 - MeshLayer 仅支持填充模式:
MeshLayer只能用于填充网格内部(Fill),不支持绘制网格的边缘轮廓(Stroke)。如果需要绘制带有边框或描边效果的图形,请改用ShapeLayer。
2D 矩阵变换
概念与动机
Matrix 是一个 3×3 单精度浮点矩阵,用于对坐标点、路径及图层执行平移、缩放、旋转、倾斜及透视投影等空间映射。所有变换均基于标准的 2D 屏幕坐标系(原点 (0, 0) 位于画布或图层的左上角,X 轴向右,Y 轴向下)。它的底层数据采用行主序(Row-Major)连续存储,其内部矩阵分量布局如下:
第0列 第1列 第2列
第0行 | SCALE_X SKEW_X TRANS_X |
第1行 | SKEW_Y SCALE_Y TRANS_Y |
第2行 | PERSP_0 PERSP_1 PERSP_2 |
- 第一和第二行:这是矩阵的仿射分量,负责平面内的平移(
TRANS_X,TRANS_Y)、缩放(SCALE_X,SCALE_Y)与倾斜/旋转(SKEW_X,SKEW_Y)。只要矩阵第三行的值为[0, 0, 1],它就是一个标准的仿射变换(变换后,原本平行的线条依然保持平行)。 - 第三行:这是矩阵的透视分量。
PERSP_0和PERSP_1分别控制 X 轴和 Y 轴方向的透视形变系数。右下角的PERSP_2是齐次坐标的全局缩放系数,通常保持为 1。
底层映射公式:
SCALE_X·x + SKEW_X·y + TRANS_X SKEW_Y·x + SCALE_Y·y + TRANS_Y
x' = ──────────────────────────────────, y' = ──────────────────────────────────
PERSP_0·x + PERSP_1·y + PERSP_2 PERSP_0·x + PERSP_1·y + PERSP_2
从映射公式可以看出,分母 w = PERSP_0·x + PERSP_1·y + PERSP_2 充当了齐次除法(Homogeneous Division)的权重因子。以 PERSP_0 为例:当 PERSP_0 > 0 时,随着输入坐标 x 值的增大,分母 w 也会随之增大,导致最终映射出的 x' 和 y' 整体随之变小。这就是平面上的几何对象在 X 轴正方向上产生"近大远小"透视形变的数学本质。
矩阵级联:
在组合多个变换时,由于矩阵乘法不遵守交换律,相乘的先后次序严格决定了图形最终的形态与位置。矩阵类提供了两种级联方式:
- preConcat(前置级联 / 局部坐标系变换):表示
M' = M × other。在应用变换时,新增加的other变换会先被执行。从图形表现上看:它等同于基于局部坐标系叠加变换。 - postConcat(后置级联 / 世界坐标系变换):表示
M' = other × M。在应用变换时,原矩阵M的变换会先被执行。从图形表现上看:它等同于基于世界坐标系叠加变换。
下图以对矩形分别执行 preConcat(前置旋转)和 postConcat(后置旋转)45° 为例,展示两者在空间效果上的差异:

注意:
Canvas上的变换操作(如canvas->concat、translate、scale等),底层实际调用的就是preConcat接口。这意味着,这些相对变换都会优先作用于你接下来要绘制的内容(图形、图像或文本等)的局部坐标系上。
关键 API 协作流程
2D 矩阵的核心使用流程如下:
// 1. 构造初始矩阵
auto matrix = Matrix::I(); // 单位矩阵
auto matrix = Matrix::MakeTrans(100.f, 50.f); // 平移矩阵
auto matrix = Matrix::MakeScale(2.f, 2.f); // 缩放矩阵
auto matrix = Matrix::MakeRotate(45.f); // 旋转矩阵
auto matrix = Matrix::MakeSkew(0.2f, 0.f); // 错切矩阵
// 2. 级联变换
matrix.preScale(2.f, 2.f); // 前置缩放
matrix.postRotate(45.f); // 后置旋转
matrix.preConcat(otherMatrix); // 前置级联另一个矩阵
matrix.invert(&inverse); // 求逆矩阵
// 3. 空间映射
matrix.mapPoints(dst, src, count); // 映射坐标点数组
auto bounds = matrix.mapRect(rect); // 映射包围盒(返回全新 AABB)
// 4. 渲染应用
auto transformedShape = Shape::ApplyMatrix(shape, matrix); // 作用于几何对象
canvas->concat(matrix); // 叠加到画布当前矩阵
canvas->setMatrix(matrix); // 替换画布当前矩阵
代码示例
下述代码通过将局部坐标系的形变(对 Shape 应用旋转和平移)与全局坐标系的变换(旋转平移 Canvas)结合,分阶段展示了三角形在不同坐标系下的变换效果:
void draw(Canvas* canvas) {
// 留出绘制间距
canvas->translate(19, 19);
// 构建初始几何:上边平行,左边垂直的直角三角形(宽高比 1:2)
Path path;
path.moveTo(0, 0);
path.lineTo(50, 0);
path.lineTo(0, 100);
path.close();
auto shape = Shape::MakeFrom(path);
// 1. 绘制最原始的三角形(灰色)
Paint paint;
paint.setColor(Color::FromRGBA(200, 200, 200, 255));
canvas->drawShape(shape, paint);
// 对 Shape 应用局部矩阵变换(旋转并平移,加宽水平间距)
auto shapeMatrix = Matrix::MakeRotate(30.0f);
shapeMatrix.postTranslate(120.0f, 0.0f);
auto transformedShape = Shape::ApplyMatrix(shape, shapeMatrix);
// 2. 绘制局部形变后的三角形(绿色)
paint.setColor(Color::FromRGBA(76, 175, 80, 255));
canvas->drawShape(transformedShape, paint);
// 构建画布的全局矩阵变换(旋转并附加平移,拉大垂直间距)
auto canvasMatrix = Matrix::MakeRotate(-15.0f);
canvasMatrix.postTranslate(-40.0f, 160.0f);
canvas->concat(canvasMatrix);
// 3. 绘制叠加画布全局变换后的最终三角形(蓝色)
paint.setColor(Color::FromRGBA(33, 150, 243, 255));
canvas->drawShape(transformedShape, paint);
}

注意事项
- 矩阵相乘不遵守交换律:先位移后旋转与先旋转后位移会得到完全不同的几何状态。可以简单地理解为:如果相对于局部坐标系做变换,新增的变换矩阵应当放到乘法等式右侧,使用
pre前缀接口;如果相对于世界坐标系做变换,应当放到等式左侧,使用post前缀接口。 - 轴对齐包围框膨胀误差:调用
mapRectAPI 时需警惕,一旦矩阵中包含旋转或错切形变,源矩形经过变换后将成为一个不再是轴对齐的任意四边形。mapRect返回的实际上是包裹此四边形的全新、更大的轴对齐包围框(AABB)。如果继续对这个已经变大的包围框应用包含旋转的mapRect,误差会不断累积导致包围框持续膨胀。若需要精确的最小轴对齐包围框,应使用mapPoints对原始四个角顶点分别进行映射变换,再手动计算这四个新顶点的最大/最小坐标来得出最终包围框。 - Canvas 矩阵叠加机制:例如执行
canvas->rotate(45)时,实际内部是在执行m.preConcat(MakeRotate(45))(此处的m代表 Canvas 当前使用的变换矩阵)。因此快捷指令始终只作用于当前的局部坐标系。如果想要直接设置一个绝对矩阵(或者完全重置画布变换),应该直接调用canvas->setMatrix()。
3D 矩阵变换
概念与动机
虽然 2D 矩阵能够通过投影系数模拟透视效果,但它无法处理真正的 Z 轴(深度)方向变换,也无法实现三维空间中的旋转。为了解决这些问题,TGFX 引入了 Matrix3D。它是一个 4×4 的单精度浮点矩阵,能够满足复杂的 3D 变换需求,常用于为图层或几何图形添加真实的 3D 空间变换效果(如三维翻转、空间透视等)。Matrix3D 的内部数据采用列主序(Column-Major)布局,其各个分量的空间几何含义如下:
第0列 第1列 第2列 第3列
第0行 | SCALE_X SKEW_X_Y SKEW_X_Z TRANS_X |
第1行 | SKEW_Y_X SCALE_Y SKEW_Y_Z TRANS_Y |
第2行 | SKEW_Z_X SKEW_Z_Y SCALE_Z TRANS_Z |
第3行 | PERS_X PERS_Y PERS_Z PERS_SCALE |
- 左上 3×3 子矩阵:控制 3D 空间中的缩放(
SCALE)与旋转/错切(SKEW)。在三维空间中,该子矩阵的三列分别代表了变换后的新坐标系的 X、Y、Z 三个方向在原坐标系中的基向量。与 2D 变换不同,3D 空间中一个坐标轴的旋转或倾斜,必然会在另外两个正交轴上产生投影分量,因此每个坐标轴都需要两个额外的SKEW分量。以第一列(第0列)为例,它代表新 X 轴在原坐标系中的向量表示:除了它在原 X 轴上的投影(SCALE_X)外,SKEW_Y_X和SKEW_Z_X分别描述了新 X 轴向 Y 轴和 Z 轴方向的偏转程度。 - 第四列的前三个元素:这是矩阵的平移分量(
TRANS_X,TRANS_Y,TRANS_Z)。从坐标系映射的角度来看,它代表了新坐标系的原点在旧坐标系(或父级坐标系)中的具体位置,负责控制对象沿着原坐标系的 X、Y、Z 轴发生的位移。 - 第四行:这是矩阵的透视投影分量。
PERS_X、PERS_Y、PERS_Z控制各个方向的透视畸变参数,PERS_SCALE则是齐次坐标的全局缩放系数(通常为 1)。
在渲染管线中,三维空间里的几何体最终在屏幕空间的投影,需要依次经历模型变换(Model Transform)、视图变换(View Transform)和透视投影变换(Perspective Transform)。其完整的矩阵变换公式为:
Matrix3D = PerspectiveMatrix * ViewMatrix * ModelMatrix
- ModelMatrix(模型矩阵):负责将对象的局部坐标系转换到世界坐标系(应用对象自身的平移、旋转、缩放)。
- ViewMatrix(视图矩阵/相机矩阵):负责将世界坐标系转换到相机观察空间,由相机的位置和朝向决定。
- PerspectiveMatrix(透视投影矩阵):负责进行透视变换,通过改变坐标的齐次权重(W 分量)来引入透视效果,从而在最终的屏幕映射上产生"近大远小"的视觉表现。
在实际的图形渲染开发中,为了构建上述的视图矩阵与透视投影矩阵,业界通常采用以下两种相机模型:
标准相机模型 (Standard Camera Model):这是 3D 图形学中的经典物理相机模型,能够完整控制相机的各项参数,模拟最符合物理规律的真实透视投影。它通过相机的空间位置、观察目标和 UP 方向决定相机的姿态,并利用视野角(FOV)、宽高比、近平面和远平面定义了一个完整的视锥体(View Frustum)。位于视锥体外部的图元都会在渲染管线中被裁剪剔除(Clipping)。相关矩阵的计算方式如下:
// 构建视图矩阵(ViewMatrix):由相机位置 (eye)、观察目标 (center) 和 UP 方向决定 ViewMatrix = Matrix3D::LookAt(eye, center, up); // 构建透视投影矩阵(PerspectiveMatrix):由视野角、宽高比、近平面和远平面定义视锥体 PerspectiveMatrix = Matrix3D::Perspective(fovyDegrees, aspect, nearZ, farZ);简洁相机模型 (Simplified Camera Model):这是为快速在 2D UI 环境中实现立体特效(如翻转、折角)而抽象出的轻量化数学模型。此模型舍弃了严谨的视锥体计算,直接利用齐次坐标机制产生缩放。在空间关系上,该模型隐式将视图矩阵(ViewMatrix)设为单位矩阵,并假定观察者位于 Z 轴正方向距离 XY 平面(Z=0)为
d的位置看向原点。此时,XY 平面(Z=0)充当了零缩放基准面——当图形的 Z 坐标为 0 时,其在屏幕上的投影大小保持原样;当 Z 坐标大于 0(靠近观察者)时,图形被放大;当 Z 坐标小于 0(远离观察者)时,图形被缩小。开发者只需指定这个观察视距(d)即可引入透视畸变。相关矩阵计算如下:// 视图矩阵隐式使用单位矩阵 ViewMatrix = Matrix3D::I(); // 透视投影矩阵仅需在 [3][2] 位置设置视距的倒数 PerspectiveMatrix = Matrix3D::I(); PerspectiveMatrix.setRowColumn(3, 2, -1.0f / d);

提示:在 TGFX 的实际业务中,绝大多数的 UI 效果渲染只需要使用简洁相机模型即可完美满足需求。
与 2D 矩阵的相互转换:
升维(2D → 3D):
Matrix3D可以直接通过 2D 的Matrix构造初始化。升维时,3×3 矩阵的各分量会被映射到 4×4 的对应位置,新增的 Z 轴相关分量填充为单位矩阵值(即 Z 轴上无变换):// 3×3 Matrix: 4×4 Matrix: // | SCALE_X SKEW_X TRANS_X | | SCALE_X SKEW_X 0 TRANS_X | // | SKEW_Y SCALE_Y TRANS_Y | → | SKEW_Y SCALE_Y 0 TRANS_Y | // | PERSP_0 PERSP_1 PERSP_2 | | 0 0 1 0 | // | PERSP_0 PERSP_1 0 PERSP_2 | Matrix3D m3d(matrix2d);降维(3D → 2D):调用
asMatrix()可将 4×4 矩阵转回 3×3 矩阵。降维时,Z 轴所在的第 2 列和第 2 行会被直接丢弃:// 4×4 Matrix: 3×3 Matrix: // | m00 m01 m02 m03 | | m00 m01 m03 | // | m10 m11 m12 m13 | → | m10 m11 m13 | // | m20 m21 m22 m23 | | m30 m31 m33 | // | m30 m31 m32 m33 | Matrix m2d = matrix3D.asMatrix();在 2D 图层系统中,这种降维丢失的信息在大多数场景下并不会影响最终的渲染结果。要理解这一点,先来看 4×4 矩阵将坐标
(x, y, z)映射到投影平面的完整公式:m00·x + m01·y + m02·z + m03 m10·x + m11·y + m12·z + m13 x' = ─────────────────────────────────, y' = ───────────────────────────────── m30·x + m31·y + m32·z + m33 m30·x + m31·y + m32·z + m33由于图层上的所有点都处于投影平面上(
z = 0),代入后所有包含z的项均变为零,即矩阵的第 2 列(m02,m12,m22,m32)对最终投影坐标没有任何影响。又因为投影平面本身的 z 坐标也为 0,我们并不需要投影结果的z'分量,所以矩阵的第 2 行(m20,m21,m22,m23)也可以安全忽略。因此,在不关心深度信息的场景下,asMatrix()降维到 2D 矩阵与使用完整 4×4 矩阵是数学等价的。但如果需要在多个图层嵌套时保持正确的深度遮挡效果,则必须使用完整的 3D 矩阵来保留
z'信息。
提示:这也是为什么画布
Canvas只提供 2D 矩阵相关接口,而图层Layer额外提供了 3D 矩阵接口:Canvas负责单层内的平面绘制,Layer则需要在图层树中维护完整的三维空间关系。
关键 API 协作流程
3D 矩阵的核心使用流程如下:
// 1. 构造初始矩阵
auto matrix3D = Matrix3D::I(); // 单位矩阵
auto matrix3D = Matrix3D::MakeTranslate(100.f, 50.f, 0.f); // 平移矩阵
auto matrix3D = Matrix3D::MakeScale(2.f, 2.f, 1.f); // 缩放矩阵
auto matrix3D = Matrix3D::MakeRotate({0.f, 1.f, 0.f}, 45.f); // 旋转矩阵(绕 Y 轴)
// 2. 级联变换
matrix3D.preScale(2.f, 2.f, 1.f); // 前置缩放
matrix3D.postRotate({0.f, 1.f, 0.f}, 45.f); // 后置旋转
matrix3D.preConcat(otherMatrix3D); // 前置级联另一个矩阵
matrix3D.setRowColumn(3, 2, -1.f / 1200.f); // 手动设置透视参数
matrix3D.invert(&inverse); // 求逆矩阵
// 3. 空间映射
auto point = matrix3D.mapPoint({x, y, z}); // 映射 3D 坐标点(透视除法后)
auto bounds = matrix3D.mapRect(rect); // 映射包围盒(返回全新 AABB)
auto dir = matrix3D.mapVector({x, y, z}); // 映射方向向量(不受平移影响)
auto homo = matrix3D.mapHomogeneous(x, y, z, w); // 映射齐次坐标(不做透视除法)
// 4. 渲染应用
auto transformedShape = Shape::ApplyMatrix3D(shape, matrix3D); // 作用于几何对象
layer->setMatrix3D(matrix3D); // 设置到图层
canvas->concat(matrix3D.asMatrix()); // 叠加到画布(注意:降维会丢失深度信息)
代码示例
示例 1:给画布设置 3D 变换
void draw(Canvas* canvas) {
auto image = Image::MakeFromFile("photo.png");
// 等比缩放到宽 200
auto imageWidth = 200.f;
auto imageHeight = imageWidth * image->height() / image->width();
image = image->makeScaled(static_cast<int>(imageWidth),
static_cast<int>(imageHeight));
// 左侧:原样绘制
auto margin = 20.f;
canvas->save();
canvas->concat(Matrix::MakeTrans(margin, margin));
canvas->drawImage(image);
canvas->restore();
// 右侧:叠加 3D 变换后绘制,中心 Y 轴与左图对齐
auto anchor = Point::Make(0.5f, 0.5f);
auto offsetToAnchor = Matrix3D::MakeTranslate(
-anchor.x * imageWidth, -anchor.y * imageHeight, 0.f);
auto invOffsetToAnchor = Matrix3D::MakeTranslate(
anchor.x * imageWidth, anchor.y * imageHeight, 0.f);
// 同时绕 X 轴和 Y 轴旋转,让 3D 效果更明显
auto rotateX = Matrix3D::MakeRotate({1.f, 0.f, 0.f}, 45.f);
auto rotateY = Matrix3D::MakeRotate({0.f, 1.f, 0.f}, -45.f);
auto perspective = Matrix3D::I();
perspective.setRowColumn(3, 2, -1.f / 800.f);
auto originTranslate = Matrix3D::MakeTranslate(
margin + imageWidth + margin, margin, 0.f);
// 矩阵从右向左依次生效:移到锚点 → 旋转 → 透视 → 移回 → 平移到目标位置
auto transform = originTranslate * invOffsetToAnchor
* perspective * rotateX * rotateY * offsetToAnchor;
canvas->save();
canvas->concat(transform.asMatrix());
canvas->drawImage(image);
canvas->restore();
}

示例 2:给图层设置 3D 变换
使用图层系统构建一个 3D 立方体:
void draw(Canvas* canvas) {
// 立方体面尺寸和组装偏移
auto faceSize = 200.f;
auto cubeOffsetX = 125.f;
auto cubeOffsetY = 90.f;
// 立方体采用"展开-折叠"模型:所有面初始共面,各面锚点设在连接边中点,
// 绕锚点旋转 ±90° 折叠成型。父层开启 preserve3D,各面按深度自动排序。
auto createFace = [&](const Point& anchor, const Vec3& axis,
float angle, const Color& color) {
auto face = SolidLayer::Make();
face->setWidth(faceSize);
face->setHeight(faceSize);
face->setColor(color);
// 构建变换:将锚点移到原点 → 绕指定轴旋转 → 移回 → 平移到组装位置
auto offsetToAnchor = Matrix3D::MakeTranslate(
-anchor.x * faceSize, -anchor.y * faceSize, 0.f);
auto invOffsetToAnchor = Matrix3D::MakeTranslate(
anchor.x * faceSize, anchor.y * faceSize, 0.f);
auto rotate = Matrix3D::MakeRotate(axis, angle);
auto originTranslate = Matrix3D::MakeTranslate(cubeOffsetX, cubeOffsetY, 0.f);
face->setMatrix3D(originTranslate * invOffsetToAnchor * rotate * offsetToAnchor);
return face;
};
// 父层:开启 preserve3D,让子层共享同一个 3D 空间
auto cubeContainer = Layer::Make();
cubeContainer->setPreserve3D(true);
// 给容器设置整体 3D 变换,让立方体以斜视角呈现
// 锚点设在立方体中心(偏移 + 半面宽/高)
auto anchorX = cubeOffsetX + faceSize * 0.5f;
auto anchorY = cubeOffsetY + faceSize * 0.5f;
auto toAnchor = Matrix3D::MakeTranslate(-anchorX, -anchorY, 0.f);
auto fromAnchor = Matrix3D::MakeTranslate(anchorX, anchorY, 0.f);
auto cubeModel = Matrix3D::I();
cubeModel.postRotate({0.f, 1.f, 0.f}, 15.f);
cubeModel.postRotate({1.f, 0.f, 0.f}, 10.f);
auto cubePerspective = Matrix3D::I();
cubePerspective.setRowColumn(3, 2, -1.f / 1200.f);
// 补偿旋转后的视觉偏移,让立方体在画布中居中
auto cubeOrigin = Matrix3D::MakeTranslate(-35.f, 25.f, 0.f);
cubeContainer->setMatrix3D(
cubeOrigin * fromAnchor * cubePerspective * cubeModel * toAnchor);
// 正面(不旋转,作为基准面)
cubeContainer->addChild(createFace({0.5f, 0.5f}, {1, 0, 0}, 0, Color::FromRGBA(220, 220, 220, 255)));
// 左面(绕左边缘向内翻折 -90°)
cubeContainer->addChild(createFace({0.0f, 0.5f}, {0, 1, 0}, -90, Color::Red()));
// 顶面(绕上边缘向内翻折 90°)
cubeContainer->addChild(createFace({0.5f, 0.0f}, {1, 0, 0}, 90, Color::Green()));
// 右面(绕右边缘向内翻折 90°)
cubeContainer->addChild(createFace({1.0f, 0.5f}, {0, 1, 0}, 90, Color::Blue()));
// 底面(绕下边缘向内翻折 -90°)
cubeContainer->addChild(createFace({0.5f, 1.0f}, {1, 0, 0}, -90, Color::FromRGBA(255, 255, 0, 255)));
// 通过 DisplayList 渲染图层树
auto displayList = std::make_unique<DisplayList>();
displayList->root()->addChild(cubeContainer);
displayList->render(canvas->getSurface());
}

注意事项
- 与 2D 矩阵共通的注意事项:3D 矩阵的级联顺序、
mapRect的包围框膨胀行为均与 2D 矩阵一致,详见 2D 矩阵注意事项。
