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 文档

自定义 Shader(RuntimeEffect)

TGFX 的内置滤镜(Blur、DropShadow 等)覆盖了大部分常见场景,但当需要实现自定义效果时,就需要直接编写 GPU Shader。RuntimeEffect 正是为此而生——通过平台原生着色语言编写自定义后处理逻辑,并以 ImageFilter 的形式无缝接入 TGFX 的渲染管线。

与 Skia SkRuntimeEffect 的区别

Skia 提供了 SkRuntimeEffect,开发者使用 SkSL(Skia 的跨平台着色语言)编写 Shader,再由 Skia 内部将 SkSL 交叉编译为各后端的原生着色语言。这种方案一次编写即可跨后端运行,但也引入了一层额外的编译抽象。

TGFX 选择了不同的路线:RuntimeEffect 直接使用当前 GPU 后端的原生着色语言——在 OpenGL/OpenGL ES 上写 GLSL,在 Metal 上写 MSL。这意味着:

  • 零翻译开销:Shader 代码直接被 GPU 驱动编译,不经过中间语言转换
  • 完整的语言能力:可以使用原生语言的全部特性,不受跨编译子集的限制
  • 更好的调试体验:GPU 报告的编译错误直接对应实际编写的代码,无需在 SkSL 和目标语言之间推测映射关系
  • 需要按后端适配:如果应用需要同时支持 OpenGL 和 Metal,需要分别编写两套 Shader 代码
核心概念

RuntimeEffect 的设计围绕三个核心类展开:

  • RuntimeEffect:抽象基类,需要继承并实现 onDraw() 方法。在 onDraw() 中可完全控制 GPU 渲染管线——创建 Shader、组装顶点数据、绑定纹理、发起绘制调用
  • CommandEncoder:GPU 命令编码器,由 TGFX 在调用 onDraw() 时传入。通过它可以获取 GPU 对象、开启 RenderPass、执行纹理拷贝等 GPU 操作
  • ImageFilter::Runtime():将 RuntimeEffect 包装为标准的 ImageFilter,使其可以通过 image->makeWithFilter() 或 Paint::setImageFilter() 接入 TGFX 的常规渲染流程

它们的协作流程如下:

继承 RuntimeEffect,实现 onDraw()
        │
        ▼
ImageFilter::Runtime(effect)  ──→  得到 ImageFilter
        │
        ▼
image->makeWithFilter(filter)  ──→  应用滤镜
        │
        ▼
canvas->drawImage(image) + context->flushAndSubmit()
        │
        ▼
TGFX 内部调用 effect->onDraw(encoder, inputTextures, outputTexture, offset)
        │
        ▼
自定义 Shader 代码在 GPU 上执行,结果写入 outputTexture
实现自定义 RuntimeEffect

继承 RuntimeEffect 并实现 onDraw() 方法。以下是一个透视变换效果的核心结构(简化自 TGFX 测试用例中的 CornerPinEffect):

class MyEffect : public RuntimeEffect {
 public:
  // 如果效果需要额外的输入图像,通过基类构造函数传入
  explicit MyEffect(const std::vector<std::shared_ptr<Image>>& extraInputs = {})
      : RuntimeEffect(extraInputs) {}

  // 可选:自定义滤镜边界计算
  // 默认实现直接返回 srcRect(输出与输入同尺寸)
  // 如果效果会改变图像尺寸(如扭曲、阴影扩展),需要重写此方法
  Rect filterBounds(const Rect& srcRect, MapDirection direction) const override {
    if (direction == MapDirection::Reverse) {
      // 反向映射:返回生成输出所需的输入区域
      return Rect::MakeLTRB(-largeValue, -largeValue, largeValue, largeValue);
    }
    // 正向映射:返回输入经滤镜处理后的输出区域
    return computeOutputBounds(srcRect);
  }

  // 核心:实现 GPU 渲染逻辑
  bool onDraw(CommandEncoder* encoder,
              const std::vector<std::shared_ptr<Texture>>& inputTextures,
              std::shared_ptr<Texture> outputTexture,
              const Point& offset) const override {
    // 1. 从 encoder 获取 GPU 对象
    auto gpu = encoder->gpu();

    // 2. 创建 Shader 和渲染管线(建议缓存以避免重复编译)
    auto pipeline = createPipeline(gpu);

    // 3. 开启 RenderPass
    RenderPassDescriptor desc(outputTexture, LoadAction::Clear,
                              StoreAction::Store, PMColor::Transparent());
    auto renderPass = encoder->beginRenderPass(desc);

    // 4. 绑定管线、顶点数据、纹理、采样器
    renderPass->setPipeline(pipeline);
    renderPass->setVertexBuffer(0, vertexBuffer);
    renderPass->setTexture(0, inputTextures[0], sampler);

    // 5. 发起绘制
    renderPass->draw(PrimitiveType::TriangleStrip, 4);
    renderPass->end();
    return true;
  }
};

onDraw() 参数说明:

参数说明
encoderGPU 命令编码器,通过 encoder->gpu() 获取 GPU 对象,通过 encoder->beginRenderPass() 开启渲染通道
inputTextures输入纹理数组。inputTextures[0] 是源图像(即 ImageFilter 的输入),inputTextures[1...n] 对应构造时传入的 extraInputs
outputTexture输出纹理,Shader 渲染结果写入此纹理
offset坐标偏移量,用于将源图像坐标映射到输出纹理坐标
创建渲染管线

在 onDraw() 中,需要通过 GPU 对象手动创建渲染管线。典型流程如下:

std::shared_ptr<RenderPipeline> createPipeline(GPU* gpu) const {
  // 1. 编写 Shader 代码(这里以 GLSL 为例)
  static constexpr char VERTEX_SHADER[] = R"(
    in vec2 aPosition;
    in vec2 aTextureCoord;
    out vec2 vTexCoord;
    void main() {
      gl_Position = vec4(aPosition, 0, 1);
      vTexCoord = aTextureCoord;
    }
  )";

  static constexpr char FRAGMENT_SHADER[] = R"(
    precision mediump float;
    in vec2 vTexCoord;
    uniform sampler2D sTexture;
    out vec4 tgfx_FragColor;
    void main() {
      vec4 color = texture(sTexture, vTexCoord);
      // 在此实现自定义效果,例如反色:
      tgfx_FragColor = vec4(1.0 - color.rgb, color.a);
    }
  )";

  // 2. 根据 GPU 后端选择 GLSL 版本头
  auto info = gpu->info();
  bool isDesktop = info->version.find("OpenGL ES") == std::string::npos;
  std::string header = isDesktop ? "#version 150\n\n" : "#version 300 es\n\n";

  // 3. 编译 Shader Module
  ShaderModuleDescriptor vertexModule = {};
  vertexModule.code = header + VERTEX_SHADER;
  vertexModule.stage = ShaderStage::Vertex;
  auto vertexShader = gpu->createShaderModule(vertexModule);

  ShaderModuleDescriptor fragmentModule = {};
  fragmentModule.code = header + FRAGMENT_SHADER;
  fragmentModule.stage = ShaderStage::Fragment;
  auto fragmentShader = gpu->createShaderModule(fragmentModule);

  // 4. 配置管线描述符
  RenderPipelineDescriptor descriptor = {};
  VertexBufferLayout vertexLayout(
      {{"aPosition", VertexFormat::Float2},
       {"aTextureCoord", VertexFormat::Float2}});
  descriptor.vertex.bufferLayouts = {vertexLayout};
  descriptor.vertex.module = vertexShader;
  descriptor.fragment.module = fragmentShader;
  descriptor.fragment.colorAttachments.push_back({});

  // 5. 声明纹理采样器绑定
  BindingEntry textureBinding = {"sTexture", 0};
  descriptor.layout.textureSamplers.push_back(textureBinding);

  return gpu->createRenderPipeline(descriptor);
}

性能提示:渲染管线的创建涉及 Shader 编译和链接,开销较大。建议在首次使用时创建并缓存管线对象,后续调用直接复用。

多纹理输入

某些效果需要多张图像参与运算,例如图像混合、纹理置换(Displacement Map)等。RuntimeEffect 通过构造函数的 extraInputs 参数支持传入额外的输入图像:

// 加载额外的输入图像(例如一张置换贴图)
auto displacementMap = Image::MakeFromFile("displacement.png");

// 将 extraInputs 传给 RuntimeEffect 构造函数
auto effect = std::make_shared<DisplacementEffect>(
    std::vector<std::shared_ptr<Image>>{displacementMap});

// 在 onDraw() 中,inputTextures 的排列顺序为:
//   inputTextures[0] = 源图像(ImageFilter 的输入)
//   inputTextures[1] = displacementMap

在 onDraw() 的 Fragment Shader 中,可以声明多个 sampler2D 来分别采样这些纹理。

作为 ImageFilter 使用

创建好 RuntimeEffect 后,使用 ImageFilter::Runtime() 将其包装为标准 ImageFilter,之后就可以像使用内置滤镜一样使用它:

// 创建自定义效果
auto effect = std::make_shared<MyEffect>();

// 包装为 ImageFilter
auto filter = ImageFilter::Runtime(std::move(effect));

// 方式 1:通过 Image 应用
auto filteredImage = image->makeWithFilter(std::move(filter));
canvas->drawImage(filteredImage, 0, 0);

// 方式 2:与其他滤镜组合
auto blurFilter = ImageFilter::Blur(5, 5);
auto composedFilter = ImageFilter::Compose(filter, blurFilter);
auto result = image->makeWithFilter(std::move(composedFilter));

以下是组合两个 CornerPinEffect(自定义 RuntimeEffect)实现图像透视变换的渲染结果:

CornerPinEffect 透视变换效果 — 两个 RuntimeEffect 组合应用

该效果将原始图像先经过恒等变换(保持原样),再经过 CornerPinEffect 映射到指定四角,产生透视投影效果。完整实现参见 TGFX 测试用例 CornerPinEffect.cpp。

MSAA 抗锯齿

如果自定义效果涉及几何变形(如透视变换),边缘可能会出现锯齿。可以通过在管线描述符中设置多重采样来启用 MSAA 抗锯齿:

// 创建多重采样纹理作为渲染目标
TextureDescriptor textureDesc(width, height, format, false,
                              4,  // sampleCount = 4
                              TextureUsage::RENDER_ATTACHMENT);
auto msaaTexture = gpu->createTexture(textureDesc);

// 渲染到 MSAA 纹理,并 resolve 到最终输出纹理
RenderPassDescriptor renderPassDesc(msaaTexture, LoadAction::Clear,
                                    StoreAction::Store, PMColor::Transparent(),
                                    outputTexture);  // resolveTexture

// 同时在管线描述符中设置采样数
descriptor.multisample.count = 4;
注意事项
  • Shader 语言按后端选择:OpenGL / OpenGL ES 后端使用 GLSL(桌面端 #version 150,移动端 #version 300 es),Metal 后端使用 MSL。如果应用需要支持多个后端,需要分别编写并在运行时根据 gpu->info()->version 选择
  • 缓存渲染管线:gpu->createRenderPipeline() 涉及 Shader 编译和链接,频繁创建会严重影响性能。务必缓存管线对象并在后续帧中复用
  • Fragment Shader 输出变量:在 GLSL 中,输出颜色变量应命名为 tgfx_FragColor,这是 TGFX 的约定
  • 颜色空间处理:当源图像和额外输入图像的颜色空间不一致时,TGFX 会自动将所有输入纹理转换到统一的颜色空间后再传入 onDraw(),无需手动处理颜色空间转换
  • filterBounds() 的重要性:如果自定义效果会改变图像尺寸(例如添加发光、扩展阴影),必须正确重写 filterBounds() 方法,否则输出可能被裁切
← 图层系统色彩管理 →
公司地址:广东省深圳市南山区海天二路33号腾讯滨海大厦Copyright © 2018 - 2026 Tencent. All Rights Reserved.联系电话:0755-86013388隐私政策