TGFX - 腾讯开源的轻量级 2D 渲染引擎

TGFX - 腾讯开源的轻量级 2D 渲染引擎

  • 首页
  • 下载
  • 文档
  • 案例
  • CN
  • GitHub
  • 论坛交流
  • Languages iconCN
    • EN

›几何与变换

快速开始

  • TGFX 简介
  • 平台与后端支持
  • 环境准备与编译
  • Hello2D 示例

API 参考与概述

    绘图基础

    • Canvas Overview
    • Paint Overview
    • Path Overview
    • BlendMode Overview
    • Picture 录制与回放

    几何与变换

    • 几何与变换

    图像与像素

    • Image
    • Bitmap 与像素操作
    • 图像编解码
    • 视频与外部纹理

    文本渲染

    • 文本与字体

    着色与效果

    • 着色与效果

    图层系统

    • 图层系统

    进阶主题

    • 自定义 Shader
    • 色彩管理

架构设计

  • 渲染管线
  • GPU 硬件抽象层
  • 图层渲染系统
  • 缓存系统
  • 文字图集渲染
  • GPU Hairline 极细描边
  • 广色域渲染
  • SIMD 加速

API 文档

  • API 文档

几何与变换


Shape

概念与动机

在 GPU 渲染管线中,矢量路径(Path)的光栅化处理涉及高昂的计算开销,通常需要将贝塞尔曲线进行线段细分、多边形三角化,或离屏光栅化为蒙版纹理。在帧渲染循环中重复计算静态路径,会导致显著的 CPU 与 GPU 性能瓶颈。

Shape 机制即为解决此性能隐患而引入。作为一种不可变的矢量图形对象,Shape 在首次计算完成后,其底层生成的顶点数据或蒙版纹理会被 GPU 缓存。后续渲染同一 Shape 实例时,渲染引擎将直接命中缓存并提交绘制,从而规避重复的几何拓扑计算开销。相比之下,Path 更适用于单次绘制或需要高频突变拓扑结构的动态场景。两者核心特性对比如下:

维度PathShape
可变性可变,允许动态修改拓扑不可变,初始化完成后状态锁定
线程安全否,缺乏并发控制是,支持安全的跨线程共享与并行构建
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);
}

旗帜 Mesh 变形效果

拓扑模式

枚举类 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 个相连的三角形。该模式避免了冗余的顶点或索引传递,非常适合连通的带状图形或矩形阵列,可大幅减少内存占用与传输带宽。

Mesh 拓扑模式示意图

注意事项

  • 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 时,才会直接输出该基础色。
  • 非归一化纹理坐标: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 前缀接口。
  • 轴对齐包围框膨胀误差:调用 mapRect API 时需警惕,一旦矩阵中包含旋转或错切形变,源矩形经过变换后将成为一个不再是轴对齐的任意四边形。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();
}

Canvas 3D 变换效果

示例 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());
}

Layer 3D 立方体效果

注意事项

  • 与 2D 矩阵共通的注意事项:3D 矩阵的级联顺序、mapRect 的包围框膨胀行为均与 2D 矩阵一致,详见 2D 矩阵注意事项。
← Picture 录制与回放Image →
  • Shape
    • 概念与动机
    • 关键 API 协作流程
    • 代码示例
    • 描边策略
    • 注意事项
  • Mesh
    • 概念与动机
    • 关键 API 协作流程
    • 代码示例
    • 拓扑模式
    • 注意事项
  • 2D 矩阵变换
    • 概念与动机
    • 关键 API 协作流程
    • 代码示例
    • 注意事项
  • 3D 矩阵变换
    • 概念与动机
    • 关键 API 协作流程
    • 代码示例
    • 注意事项
公司地址:广东省深圳市南山区海天二路33号腾讯滨海大厦Copyright © 2018 - 2026 Tencent. All Rights Reserved.联系电话:0755-86013388隐私政策