自定义 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() 参数说明:
| 参数 | 说明 |
|---|---|
encoder | GPU 命令编码器,通过 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 映射到指定四角,产生透视投影效果。完整实现参见 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()方法,否则输出可能被裁切
