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

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

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

›架构设计

快速开始

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

API 参考与概述

    绘图基础

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

    几何与变换

    • 几何与变换

    图像与像素

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

    文本渲染

    • 文本与字体

    着色与效果

    • 着色与效果

    图层系统

    • 图层系统

    进阶主题

    • 自定义 Shader
    • 色彩管理

架构设计

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

API 文档

  • API 文档

广色域渲染

第二章「色彩管理」介绍了 ColorSpace API 的使用方式。本节深入解析 TGFX 内部如何实现色彩空间转换——从转换步骤的计算与优化,到 GPU Shader 代码的动态生成,再到 CPU 端批量像素转换,以及 SVG / PDF 导出时的色彩空间嵌入策略。

色域范围对比

不同色彩空间覆盖的可见光范围差异显著。以 CIE 1931 xy 色度图为参考,四种常用色彩空间的色域覆盖如下:

CIE 1931 色度图 — 色域范围对比

色彩空间色域基色相对 sRGB 面积典型应用
sRGB / Rec.709R(0.64, 0.33) G(0.30, 0.60) B(0.15, 0.06)100%Web、标准显示器
Display P3 (D65)R(0.680, 0.320) G(0.265, 0.690) B(0.150, 0.060)~125%Apple 设备、HDR 内容
Adobe RGBR(0.64, 0.33) G(0.21, 0.71) B(0.15, 0.06)~115%印刷、专业摄影
Rec.2020R(0.708, 0.292) G(0.170, 0.797) B(0.131, 0.046)~176%UHD/HDR 视频

Display P3 在红色和绿色方向上比 sRGB 扩展了约 25% 的色域面积,能表达更鲜艳的红色和更饱和的绿色。Adobe RGB 则主要在青绿色区域扩展。Rec.2020 覆盖了人眼可见色域的绝大部分,是 HDR 视频的标准色彩空间。

以下是通过 SVG 定义的多种广色域颜色(Display P3、Rec.2020 等),渲染到 Display P3 Surface 上的实际效果:

广色域色块渲染效果 — 不同色彩空间的颜色在 Display P3 Surface 上的表现

上排为 Display P3 色域的绿色、红色、蓝色,下排为其他广色域颜色。在支持广色域的显示器上,这些色块比标准 sRGB 颜色更加鲜艳饱和。

TGFX 在 ColorSpace.h 中以 ColorSpacePrimaries 结构定义了上述所有色域基色的精确坐标(NamedPrimaries::Rec709、NamedPrimaries::SMPTE_EG_432_1、NamedPrimaries::Rec2020 等),并通过 ColorSpacePrimaries::toXYZD50() 方法将色域基色转换为 CIE XYZ D50 矩阵——这是所有色域变换的中间参考空间。

ColorSpaceXformSteps:转换步骤计算

ColorSpaceXformSteps(位于 src/core/ColorSpaceXformSteps.h)是色彩空间转换的核心数据结构。它在构造时根据源和目标色彩空间的属性,计算出一个最优的转换管线。

完整管线

一次色彩空间转换最多包含 7 个步骤:

源像素 (编码值)
    │
    ▼
① Unpremultiply ── 解除 alpha 预乘(仅当源为 Premultiplied 时)
    │
    ▼
② Linearize ────── 应用源传输函数(编码值 → 线性光值)
    │
    ▼
③ Source OOTF ──── 应用源光-光传输函数(仅 HLG 等 HDR 传输函数)
    │
    ▼
④ Gamut Transform ─ 3×3 色域矩阵变换(源 XYZ → 目标 XYZ)
    │
    ▼
⑤ Dest OOTF ────── 应用目标光-光传输函数的逆(仅 HLG 等 HDR 传输函数)
    │
    ▼
⑥ Encode ────────── 应用目标传输函数的逆(线性光值 → 编码值)
    │
    ▼
⑦ Premultiply ──── 重新 alpha 预乘(仅当目标为 Premultiplied 时)
    │
    ▼
目标像素 (编码值)

其中 OOTF(Opto-Optical Transfer Function)是 HLG 等 HDR 传输函数特有的步骤,用于在不同亮度的显示设备间进行场景参考与显示参考之间的映射。对于 sRGB 和 Display P3 等 SDR 色彩空间,OOTF 步骤不会被启用。

传输函数的类型化处理

构造函数根据 skcms_TransferFunction_getType() 判断源/目标传输函数的类型,分三种情况处理:

传输函数类型典型代表处理方式
sRGBishsRGB、Rec.709、2.2 gamma直接使用 ICC 7 参数传输函数
PQishRec.2100 PQ (HDR)使用专用 PQ 传输函数,按 10000 nit 峰值亮度缩放
HLGishRec.2100 HLG (HDR)使用专用 HLG 传输函数,按峰值/参考白亮度缩放,可能启用 OOTF
自动优化:跳过冗余步骤

构造函数的关键设计是自动剪枝——跳过不必要的步骤以提高性能:

  1. 相同色彩空间:srcHash == dstHash && srcAT == dstAT 时直接返回,所有 flags 保持 false,不执行任何操作

  2. 相同传输函数:如果源和目标的 transferFunctionHash 相同,Linearize 和 Encode 步骤互相抵消,两个 flag 都设为 false。这意味着 sRGB → Display P3 转换只需要做色域矩阵变换,无需 gamma 编解码

  3. OOTF 互消:如果源和目标都有 OOTF、且没有色域变换、且 gamma 互逆((srcGamma+1) × (dstGamma+1) == 1),两个 OOTF 步骤互相抵消

  4. Premul 跳过:如果在 Unpremul 和 Premul 之间没有非线性操作(既不需要 Linearize 也不需要 Encode),那么 Unpremul-Premul 对可以整体跳过

这些优化在构造时一次性完成,后续无论是 GPU Shader 代码生成还是 CPU 逐像素计算,都只执行真正需要的步骤。

色域矩阵计算

色域变换的核心是一个 3×3 矩阵 srcToDstMatrix。计算过程:

  1. 源色彩空间通过 toXYZD50() 得到 srcToXYZ 矩阵
  2. 目标色彩空间通过 toXYZD50() 得到 dstToXYZ 矩阵,再求逆得到 XYZtoDst
  3. 两者相乘得到 srcToDst = XYZtoDst × srcToXYZ

GPU 端:Shader 代码动态生成

GPU 端的色彩空间转换通过 ColorSpaceXformEffect(Fragment Processor)和 ColorSpaceXformHelper(Shader 代码生成辅助类)实现。整个过程是一套"元编程"系统——根据 ColorSpaceXformSteps 的 flags 动态生成 GLSL/MSL Shader 代码。

架构分层
ColorSpaceXformEffect (FragmentProcessor)
    │
    ├── emitCode()    → 生成 Shader 代码
    │       │
    │       ▼
    │   ColorSpaceXformHelper.emitCode()
    │       │
    │       ▼
    │   ShaderBuilder.appendColorGamutXform()
    │       │
    │       ├── emitTFFunc()      → 生成传输函数    float src_tf(float x) { ... }
    │       ├── emitOOTFFunc()    → 生成 OOTF 函数  vec3 src_ootf(vec3 color) { ... }
    │       ├── gamut_xform()     → 生成色域变换     color.rgb = ColorXform * color.rgb
    │       └── color_xform()     → 组装完整转换     vec4 color_xform(vec4 color) { ... }
    │
    ├── onSetData()   → 上传 Uniform 数据
    │       │
    │       ▼
    │   ColorSpaceXformHelper.setData()
    │       │
    │       ├── SrcTF0/SrcTF1    → float4 × 2(传输函数 7 参数)
    │       ├── SrcOOTF          → float4(OOTF Y 系数 + gamma)
    │       ├── ColorXform       → float3x3(色域矩阵)
    │       ├── DstOOTF          → float4
    │       └── DstTF0/DstTF1    → float4 × 2
    │
    └── onComputeProcessorKey() → 生成缓存 Key
Shader 代码生成细节

ShaderBuilder::appendColorGamutXform() 是代码生成的核心。它根据 ColorSpaceXformHelper 的 flags 按需生成辅助函数并组装成一个完整的 color_xform() 函数:

传输函数生成(emitTFFunc):根据传输函数类型生成不同的数学公式——

  • sRGBish:分段函数 x < D ? C*x + F : pow(A*x + B, G) + E
  • PQish:pow(max(A + B*pow(x, C), 0) / (D + E*pow(x, C)), F)
  • HLGish:x*A <= 1 ? pow(x*A, B) : exp((x - E)*C) + D
  • HLGinvish:x <= 1 ? A*pow(x, B) : C*log(x - D) + E

所有函数都处理了负值(取绝对值计算后恢复符号),确保广色域中可能出现的超出 [0,1] 的值不会产生 NaN。

色域变换生成(gamut_xform):生成一个简单的 3×3 矩阵乘法 color.rgb = ColorXform * color.rgb,矩阵值通过 Uniform float3x3 传入。

完整转换函数(color_xform):按顺序组装所有启用的步骤。代码结构如下(伪代码):

vec4 color_xform(vec4 color) {
    // ① Unpremultiply(仅当 flags.unPremul 为 true 时生成)
    float alpha = color.a;
    color = alpha > 0.0 ? vec4(color.rgb / alpha, alpha) : vec4(0.0);

    // ② Linearize(仅当 flags.linearize 为 true 时生成)
    color.r = src_tf(color.r);
    color.g = src_tf(color.g);
    color.b = src_tf(color.b);

    // ③ Source OOTF(仅当 flags.srcOOTF 为 true 时生成)
    color.rgb = src_ootf(color.rgb);

    // ④ Gamut Transform(仅当 flags.gamutTransform 为 true 时生成)
    color = gamut_xform(color);

    // ⑤ Dest OOTF(仅当 flags.dstOOTF 为 true 时生成)
    color.rgb = dst_ootf(color.rgb);

    // ⑥ Encode(仅当 flags.encode 为 true 时生成)
    color.r = dst_tf(color.r);
    color.g = dst_tf(color.g);
    color.b = dst_tf(color.b);

    // ⑦ Premultiply(仅当 flags.premul 为 true 时生成)
    color.rgb *= color.a;

    return color;
}

由于 Shader 代码是动态生成的,跳过的步骤不会出现在最终的 Shader 中,因此没有分支开销。例如 sRGB → Display P3 转换只会生成 Unpremul → GamutXform → Premul 三步(传输函数相同被跳过)。

Shader 缓存与 Key 机制

ColorSpaceXformSteps::XFormKey() 将转换步骤编码为一个 32-bit 整数:低 8 位是 flags mask(哪些步骤启用),第 8-15 位编码源传输函数类型,第 16-23 位编码目标传输函数类型。这确保了相同转换步骤的 Shader 可以被 GPU Pipeline 缓存系统复用。

触发转换的入口

GPU 端色彩空间转换通过 ColorSpaceXformEffect 这个 FragmentProcessor 注入到渲染管线。注入点有三类:

  1. Image 绘制(ImageShader::asFragmentProcessor):当 Image 的色彩空间与 Surface 的色彩空间不同时,在纹理采样的 FragmentProcessor 之后 Compose 一个 ColorSpaceXformEffect

  2. DrawImage 直接绘制(OpsCompositor):与 ImageShader 路径类似,但发生在 OpsCompositor 的 DrawOp 构建流程中

  3. RuntimeEffect 输入纹理对齐(RuntimeDrawTask::GetFlatTextureView):当多个输入纹理的色彩空间不一致时,在扁平化纹理的过程中插入色彩空间转换,确保传入 onDraw() 的所有纹理处于同一色彩空间

  4. 顶点颜色转换(RectsVertexProvider):当矩形绘制带有顶点颜色且目标色彩空间非 sRGB 时,通过 CPU 端 ColorSpaceXformSteps::apply() 在写入顶点缓冲区时直接转换颜色值,避免在 Shader 中额外处理

  5. 渐变色转换(GradientShader):渐变色在 CPU 端通过 ColorSpaceXformSteps::apply() 将 sRGB 颜色值预转换到目标色彩空间,然后直接写入 Uniform 数据

CPU 端:skcms 批量像素转换

CPU 端的色彩空间转换分为两条路径:

逐像素转换

ColorSpaceXformSteps::apply(float rgba[4]) 方法在 CPU 上对单个像素执行与 GPU Shader 完全相同的转换管线。它直接调用 skcms_TransferFunction_eval() 计算传输函数、用 3×3 矩阵做色域变换。这条路径用于少量颜色的转换,例如渐变色预处理和顶点颜色转换。

批量行级转换

CopyPixels()(位于 src/core/utils/CopyPixels.cpp)是 CPU 端批量像素转换的入口。它将源和目标色彩空间转换为 skcms_ICCProfile,然后逐行调用 skcms_Transform():

auto srcProfile = ToSkcmsICCProfile(srcColorSpace);
auto dstProfile = ToSkcmsICCProfile(dstColorSpace);
for (int i = 0; i < height; i++) {
    skcms_Transform(srcPixels, srcFormat, srcAlpha, &srcProfile,
                    dstPixels, dstFormat, dstAlpha, &dstProfile,
                    width);
    // 移动到下一行...
}

skcms_Transform() 是 Google 的 skcms 库 提供的高性能色彩转换函数,内部针对 x86 SSE/AVX 和 ARM NEON 做了 SIMD 优化。它同时处理色彩空间转换和像素格式转换(如 RGBA_8888 → RGBA_F16),避免了两次遍历。

ConvertColorSpaceInPlace() 是就地转换的便捷封装:

void ConvertColorSpaceInPlace(int width, int height, ColorType colorType,
                              AlphaType alphaType, size_t rowBytes,
                              const shared_ptr<ColorSpace>& srcCS,
                              const shared_ptr<ColorSpace>& dstCS,
                              void* pixels) {
    auto srcInfo = ImageInfo::Make(width, height, colorType, alphaType, rowBytes, srcCS);
    auto dstInfo = srcInfo.makeColorSpace(dstCS);
    CopyPixels(srcInfo, pixels, dstInfo, pixels);  // 源和目标指向同一内存
}

这条路径的典型使用场景包括:各平台 NativeCodec 解码后的色彩空间转换、Surface::readPixels() 的像素回读、以及 Web 平台 Canvas 光栅化结果的后处理。

NeedConvertColorSpace:转换判定

NeedConvertColorSpace() 是贯穿整个系统的判定函数,决定是否需要进行色彩空间转换:

bool NeedConvertColorSpace(const shared_ptr<ColorSpace>& src,
                           const shared_ptr<ColorSpace>& dst) {
    if (dst == nullptr) return false;
    ColorSpace* newSrc = src ? src.get() : ColorSpace::SRGB().get();
    return !ColorSpace::Equals(newSrc, dst.get());
}

设计要点:

  • 目标为 nullptr 时不转换——这是「不关心色彩空间」的语义
  • 源为 nullptr 时默认按 sRGB 处理——这确保了未标注色彩空间的内容在广色域 Surface 上也能正确显示
  • ColorSpace::Equals() 使用 hash 快速判等,避免逐字段比较

SVG 导出中的色彩空间处理

SVG 导出时的色彩空间处理通过 targetColorSpace 和 assignColorSpace 两个参数控制,分别对应"转换"和"指派"两种语义:

  • targetColorSpace:将所有颜色值从 sRGB 转换到目标色彩空间后写入 SVG。这会改变颜色的数值
  • assignColorSpace:不转换颜色值,仅在像素数据中嵌入指定的色彩空间标记
颜色值转换

ElementWriter 在写入 SVG 元素时,对所有颜色值(fill、stroke、阴影颜色、渐变色等)调用 ConvertColorSpace() 将 sRGB 颜色转换到目标色彩空间。渐变色则通过 ColorSpaceXformSteps::apply() 在 CPU 端逐色标转换。

图像像素转换

图像数据通过 ConvertImageColorSpace() 函数处理:先创建一个目标色彩空间的 Surface,将图像绘制上去(GPU 自动完成色彩空间转换),再 readPixels() 回读转换后的像素数据,最终编码为 PNG 嵌入 SVG。

PDF 导出中的色彩空间嵌入

PDF 对色彩空间有更严格的规范要求。TGFX 的 PDF 导出采用 ICC Profile 嵌入的方式:

ICC Profile 序列化

PDFDocumentImpl::emitColorSpace() 将目标色彩空间导出为 ICC Profile 数据并嵌入 PDF:

                    ┌──────────────────────┐
ColorSpace          │  PDFStream           │
    │               │  /Type /ICCBased     │
    ▼               │  /N 3               │
toICCProfile() ──→  │  /Alternate DeviceRGB│
                    │  [ICC binary data]   │
                    └──────────────────────┘
                              │
                              ▼
                    PDFArray ["ICCBased", ref]
                              │
                              ▼
              ResourceDict /ColorSpace /CS ref

每个页面的资源字典中都引用这个全局的 ICC Profile 对象(/ColorSpace /CS),确保 PDF 阅读器能正确解释所有颜色值。

像素数据处理

PDFBitmap::SerializeImage() 在序列化图像时,创建一个目标色彩空间的 Surface 来绘制源图像,然后以 Unpremultiplied 格式回读像素——PDF 规范要求图像数据为非预乘格式。如果设置了 colorSpaceRef,图像的 ColorSpace 字典会引用该 ICC Profile;否则回退到 DeviceRGB。

渐变色处理

PDFGradientShader 在生成渐变色 PDF 对象时,也会检查 NeedConvertColorSpace(),并通过 ColorSpaceXformSteps::apply() 在 CPU 端将 sRGB 渐变色标转换到目标色彩空间。

与 Skia 的方案对比

维度TGFXSkia
色彩空间库使用 skcms(Skia 的子项目),CPU 端共用同一底层同样使用 skcms
GPU 转换机制ColorSpaceXformEffect (FragmentProcessor) 动态生成 GLSL/MSLGrColorSpaceXformEffect (GrFragmentProcessor),机制类似
Shader 代码生成直接在 ShaderBuilder 中拼接 GLSL 字符串通过 SkSL 跨编译器生成,多一层抽象
步骤优化构造时一次性剪枝,7 种优化规则类似的构造时优化策略
PDF/SVG 嵌入直接将 ColorSpace 序列化为 ICC Profile 嵌入Skia 的 PDF 后端采用类似的 ICC Profile 嵌入方式
← GPU Hairline 极细描边SIMD 加速 →
公司地址:广东省深圳市南山区海天二路33号腾讯滨海大厦Copyright © 2018 - 2026 Tencent. All Rights Reserved.联系电话:0755-86013388隐私政策