news 2026/9/24 14:28:46

Skia SkSL 与 Runtime Effects 完整实战指南:从着色语言语法到色彩管理、预乘 Alpha 与代码最小化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Skia SkSL 与 Runtime Effects 完整实战指南:从着色语言语法到色彩管理、预乘 Alpha 与代码最小化
  • 图形学

【免费下载链接】skia

Skia is a complete 2D graphic library for drawing Text, Geometries, and Images. See documentation for contribution instructions.

项目地址:https://gitcode.com/gh_mirrors/ski/skia
点击查看免费下载

SkSL(Skia Shading Language)是 Skia 的着色语言,而SkRuntimeEffect是 Skia 提供的 C++ 对象,可用来创建由 SkSL 代码控制行为的SkShaderSkColorFilterSkBlender。本文以 site/docs/user/sksl.md 为骨架,结合 include/effects/SkRuntimeEffect.h、src/sksl/sksl_public.sksl、include/core/SkImage.h 与 tools/sksl-minify/SkSLMinify.cpp 等源码,系统讲解 Runtime Effects 的入口函数约定、子着色器求值、坐标与色彩空间处理、预乘 Alpha 语义以及sksl-minify最小化工具的使用方法。读完本文,你将能够熟练编写并集成 SkSL Runtime Effects,正确处理颜色与坐标,并用官方工具压缩你的着色器代码。

一、SkSL 与 Runtime Effects 概述

SkSL 语法与 GLSL 高度相似,你可以直接在 shaders.skia.org 上在线实验。但在 Skia 应用中使用 SkSL 效果时,存在一些与 GLSL 的关键差异,这些差异大多源于一个基本事实:

对于 GPU 着色语言,你编程的是GPU 流水线的一个阶段;而对于 SkSL,你编程的是Skia 流水线的一个阶段

GLSL 的片段着色器完全控制 GPU 从光栅化到混合硬件之间的行为——它计算出颜色,并将该颜色原样送入固定功能的混合阶段。而 SkSL 效果只是更大 Skia 流水线的一部分:当你发起一次 canvas 绘制操作时,Skia(通常)会组装出单个GPU 片段着色器来完成所有必要的工作,该着色器通常包含多个片段:

  • 判断像素是否落在被绘制形状内部或外部(或在边界上,此时可能应用抗锯齿);
  • 判断像素是否落在裁剪区域内部或外部(同样可能有针对边界像素的抗锯齿逻辑);
  • SkPaintSkShader的逻辑——由于SkShaders::Blend等特性,SkShader实际上可以是一棵对象树;
  • SkColorFilter的类似逻辑(同样可以是一棵树,如SkColorFilters::ComposeSkColorFilters::Blend);
  • 混合代码(针对某些SkBlendMode,或通过SkPaint::setBlender指定的自定义混合);
  • 作为 Skia 色彩管理 一部分的颜色空间转换代码。

即使SkPaintSkShaderSkColorFilterSkBlender字段中有着复杂的对象树,仍然只有一个GPU 片段着色器:树中的每个节点各创建一个函数,裁剪代码和几何代码各创建一个函数,混合代码可能创建一个函数,最终的整体片段着色器调用所有这些函数(函数之间还可以互相调用,例如SkShader树的情形)。

你的 SkSL 效果就是为 GPU 片段着色器贡献一个函数。

从源码看,这一模型的入口约定定义在 include/effects/SkRuntimeEffect.h 中:

  • Shader 效果:入口函数形如vec4 main(vec2 inCoords),返回的颜色应为预乘(premultiplied)颜色;
  • ColorFilter 效果:入口函数形如vec4 main(vec4 inColor)
  • Blender 效果:入口函数形如vec4 main(vec4 srcColor, vec4 dstColor)

头文件同时指出,这些vec4参数与返回值实际上可以写成half4float4vec4float4的别名),编译时MakeForShaderMakeForColorFilterMakeForBlender会校验 SkSL 代码对该流水线阶段是否合法。

二、评估(采样)其他 SkShader

在 GLSL 中,片段着色器可以采样纹理。而在 Runtime Effects 中,你在 C++ 侧绑定的对象是SkShader,在 SkSL 中以shader表示。为了强调你操作的是一个会生成自身着色器代码的对象,你不使用sample,而是调用shader对象的.eval()方法。同时,Skia 提供了从SkImage创建SkShader的简单方法,因此在 Runtime Effect 中使用图片非常容易。

由于绑定并求值的是SkShader,你无需先把内容转成图片(纹理),就能直接使用任意 Skia 着色器。例如:

  • 可以直接求值一个线性渐变——Skia 不会为渐变创建纹理,而是生成一个同时计算渐变颜色、从图片纹理采样、再将两者相乘的单一片段着色器;
  • 甚至可以调用另一个 Runtime Effect,从而动态组合着色器片段。

这种“着色器树”的求值模型正是第一节所述“每个节点创建一个函数”的体现:被求值的子着色器会以内联函数的形式被并入最终片元着色器,而不是通过额外的纹理采样间接完成。Skia 对这些子着色器的绑定能力也反映在SkRuntimeEffect::ChildPtr的设计中——它可持有SkShaderSkColorFilterSkBlender,对应的 SkSL 子对象类型由ChildTypekShaderkColorFilterkBlender)描述,见 include/effects/SkRuntimeEffect.h。

三、坐标空间

要理解 SkSL 中的坐标,首先需要理解 Skia 中的坐标系。之后只需记住:传递给main()的坐标是局部(local)坐标,相对于SkShader自身的坐标空间,与 canvas 的局部空间以及任何localMatrix变换保持一致;此外,如果该着色器被另一个着色器调用,父着色器可以任意修改这些坐标。

另一个关键区别:由SkImage生成的SkShader不使用归一化坐标(不像 GLSL 中的纹理)。它的坐标原点(0, 0)在左上角,(w, h)在右下角。通常这正是你想要的——如果你用传入的坐标直接求值一个SkImageShader,缩放是正确的。但当你想要调整这些坐标(对图片做某种重映射)时,请记住坐标是按图片的实际尺寸缩放的。

四、色彩空间与 SkSL

使用 Skia 的应用通常是色彩管理的:目标 surface 的颜色空间决定了一次绘制的工作颜色空间;源内容(如 shader,包括SkImageShader)也有自己的颜色空间。默认情况下,输入到你 SkSL shader 的颜色都会被转换到工作颜色空间。但有些输入需要特别处理,才能获得或抑制这种行为。

一个典型例子:同一张 mandrill 图片,第一次正常绘制,尊重文件内存储的颜色空间(恰好是 sRGB);第二次给图片指定 Rec. 2020 颜色空间——这只是告诉 Skia“把图片中的颜色当作 Rec. 2020 来处理”,Skia 随后将这些值从 Rec. 2020 转换到目标 surface 的颜色空间(sRGB),于是颜色显得更加鲜艳。更重要的是,如果图片或目标 surface 真的处于其他颜色空间,这种自动转换可以保证内容在任何用户的屏幕上看起来一致正确。

4.1 Uniforms 与 layout(color)

Skia 和 SkSL 不知道你的uniform变量是否包含颜色,因此不会自动对它们应用颜色转换。解决方案是用layout(color)声明:

  • layout(color)只能用于类型为vec3(RGB)或vec4(RGBA)的 uniform;
  • 提供 uniform 数据时,颜色应使用未预乘的 sRGB 颜色
  • 如果你想提供广色域颜色,这些值可以超出[0, 1]范围——这与 Skia 在SkPaint上接收和存储颜色的方式一致;
  • 当 SkSL 执行时,Skia 会将该 uniform 值转换到工作颜色空间

从源码看,这一行为对应SkRuntimeEffect::Uniform::Flags中的kColor_Flag(值为0x2),其注释明确写道:声明了layout(color)的 uniform,颜色应作为未预乘、扩展范围(不裁剪)的 sRGB(即SkColor4f)提供,并会被自动转换为未预乘、扩展范围的工作空间颜色,见 include/effects/SkRuntimeEffect.h。

对比两种声明的效果:对未标注的not_a_color,Skia 把原始浮点值直接喂给 SkSL,因此执行时not_a_color始终是{1,0,0,1},与 surface 颜色空间无关,会得到目标颜色空间中最鲜艳的红色;而对标注了layout(color)color,起始为 sRGB 红,执行时会被转换为 Rec. 2020 工作空间中表示同一颜色的值。整体效果是:正确标注的 uniform 看起来更“暗”,但这正是你想要的——标注为颜色的 uniform 无论目标 surface 处于什么颜色空间,都代表同一一致的色彩。

4.2 原始图片着色器(Raw Image Shaders)

大多数图片包含应被色彩管理的颜色,但也有些图片存储的数据并非颜色,例如法线、材质属性(粗糙度等)、高度图或其他恰好存进图片的纯数学数据。在 SkSL 中使用这类图片时,应使用raw image shader,通过SkImage::makeRawShader创建,见 include/core/SkImage.h。它与普通 image shader 行为相似(包括过滤和平铺),但有几点重大差异:

  • 永远不会应用颜色空间转换(图片的颜色空间被忽略);
  • alpha 类型为kUnpremul的图片不会被自动预乘;
  • 不支持双三次过滤——调用makeRawShader请求双三次过滤会返回nullptr

示例:创建一张球面法线贴图,配合光照 shader 渲染到不同颜色空间。若用普通 image shader,法线会被当作颜色转换到工作颜色空间,从而被错误地改变;最后一次绘制使用 raw image shader,则返回原始法线,完全忽略工作颜色空间。

4.3 在已知颜色空间中工作

在 SkSL shader 内部,你并不知道工作颜色空间是什么。对很多效果来说这没问题——求值 image shader、做简单颜色运算通常会得到合理结果(尤其当你知道应用的工作颜色空间总是 sRGB 时)。但对某些效果,在固定已知的颜色空间中做运算很重要。最常见的例子是光照:物理精确的光照应当在线性颜色空间中进行。

为此 SkSL 提供了两个内建函数:

vec3 toLinearSrgb(vec3 color); vec3 fromLinearSrgb(vec3 color);

它们在当前工作颜色空间与线性 sRGB 颜色空间之间转换。该空间使用 sRGB 色域(primaries)和线性传递函数,用扩展范围值(低于 0.0、高于 1.0)表示 sRGB 色域之外的值。这对应 Android 的LINEAR_EXTENDED_SRGB或 Apple 的extendedLinearSRGB

从源码看,这两个内建函数定义在 src/sksl/sksl_public.sksl,被标注为$pure并且是“不属于 GLSL 的 SkSL 内建函数”,源码注释同样说明它们是“工作(目标)空间与固定(已知)空间之间的颜色空间转换”。在线实验里可以看到一个球体示例:光照运算分别在工作空间(sRGB)与线性空间中进行,对比两者差异。

五、预乘 Alpha(Premultiplied Alpha)

处理透明颜色时,有**未预乘(straight)预乘(premultiplied)**两种常见表示。在 Skia 流水线中,每个SkShader返回的都是预乘颜色

如果你熟悉 OpenGL 混合,可以从混合方程的角度理解:对于常见的 alpha 混合(source-over),你通常会配置混合函数为(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);而 Skia 将 source-over 混合定义为仿佛混合函数是(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)——因为源颜色已经预乘了 alpha。

Skia 使用预乘 alpha 意味着:

  • 如果你从一张未预乘的SkImage(如 PNG)创建SkImageShader并求值它……得到的颜色是[R*A, G*A, B*A, A]不是[R, G, B, A]
  • 如果你的 SkSL 会返回透明颜色,必须确保将RGB乘以A
  • 对于更复杂的 shader,你必须清楚哪些颜色是预乘的、哪些是未预乘的。把两类颜色混在一起运算往往没有意义。

正确预乘的颜色会随着 alpha 减小产生平滑的渐变;而未预乘的颜色则会让渐变显示错误——随着 alpha 变化变得过亮并发生色相偏移。这也是 include/effects/SkRuntimeEffect.h 中MakeForShader注释强调“返回的颜色应为预乘”的原因。

六、最小化 SkSL:sksl-minify 工具

Skia 自带一个最小化工具,可自动减小 Runtime Effect 或 SkMesh 代码的体积。该工具会:

  • 消除空白和注释;
  • 缩短函数名与变量名;
  • 删除未被引用的代码。

官方示例显示,一段 shader 代码最小化后可缩减到约原始大小的一半,同时渲染结果完全相同。

6.1 启用与构建

要启用该工具,在你的 gn 参数列表中添加skia_compile_modules = true

  • 命令行中可通过gn args out/yourbuild访问参数,或直接编辑文件out/yourbuild/args.gn
  • 然后用ninja重新编译 Skia;
  • 编译完成后,输出目录中会出现一个名为sksl-minify的新工具。

6.2 命令行用法

sksl-minify的调用形式如下(输出路径在前,输入路径在后):

sksl-minify <output> <input> [--frag|--vert|--compute|--shader|--colorfilter|--blender|--meshfrag|--meshvert] [dependencies...]

各参数说明(与 tools/sksl-minify/SkSLMinify.cpp 中的命令行解析逻辑对应):

  • 输出路径(第一个位置参数),例如MyShader.minified.sksl
  • 输入路径(第二个位置参数),例如MyShader.sksl
  • --stringify(可选):将最小化后的 SkSL 文本包装成一个带引号的 C++ 字符串。默认情况下输出文件包含纯 SkSL 文本;文档示例代码中的最小化 shader 字符串就是用--stringify生成的。从源码看,启用该选项时输出形如static constexpr char SKSL_MINIFIED_<输入文件名>[] = "...",见 tools/sksl-minify/SkSLMinify.cpp;
  • 程序类型选项(可选,互斥):--shader--colorfilter--blender--meshfrag--meshvert,默认值是--shader。源码还额外支持--frag--vert--compute--privshader等内部/调试选项,且多个程序类型标志同时出现时报错并打印 usage。

6.3 最小化 SkMesh 程序的注意事项

当最小化一个 mesh 程序时,你必须提供与SkMeshSpecification对应的struct Varyingsstruct Attributes。这两个结构体会被从最小化后的程序中剔除,方便使用——因为它们是经由SkMeshSpecification合成的。这一行为在源码中有明确实现:对于 mesh 片段/顶点程序,名为AttributesVaryingsStructDefinition会被跳过而不输出,见 tools/sksl-minify/SkSLMinify.cpp。

七、从 C++ 侧创建 Runtime Effects

理解了 SkSL 语言本身后,C++ 侧的集成同样重要。SkRuntimeEffect是效果对象的“基类”,而SkRuntimeEffectBuilder是简化创建过程的实用工具,它帮你管理 uniform 数据块,并提供对 uniform 变量和子着色器槽位的按名访问。其头文件中的用法示例(include/effects/SkRuntimeEffect.h):

sk_sp<SkRuntimeEffect> effect = ...; SkRuntimeEffectBuilder builder(effect); builder.uniform("some_uniform_float") = 3.14f; builder.uniform("some_uniform_matrix") = SkM44::Rotate(...); builder.child("some_child_effect") = mySkImage->makeShader(...); ... sk_sp<SkShader> shader = builder.makeShader(nullptr, false);

要点:

  • 调用makeShader/makeColorFilter/makeBlender时,builder 会校验 SkSL 入口函数是否合法;
  • uniform(name)返回的赋值操作符不做类型转换——值的大小必须与效果期望的完全一致(例如SkMatrix会按 3×3 布局写入,见头文件中的重载),否则 debug 构建会直接报错;
  • 底层 uniform 数据通过SkData::MakeZeroInitialized(effect->uniformSize())分配,uniformSize()返回所有uniform变量占用的总字节数,见 include/effects/SkRuntimeEffect.h;
  • builder 完全构建在SkRuntimeEffect的公共 API 之上,因此既可以原样使用,也可以作为你自定义绑定接口的参考实现。

此外,SkRuntimeEffect还提供uniforms()children()findUniform(name)findChild(name)等反射查询接口,以及MakeTraced(对 shader 在指定坐标生成执行轨迹,用于调试,仅支持光栅 canvas)等高级能力,可参考头文件完整声明。

结语

SkSL 是 Skia 流水线中的“函数贡献者”:它不同于直接面对 GPU 的 GLSL,而是嵌入 Skia 更大着色器图的一环。使用 Runtime Effects 时,请牢牢把握三条主线:坐标是局部的、颜色默认会被色彩管理、返回颜色必须是预乘的;对于非颜色数据使用makeRawShader,对于需要物理正确性的运算使用toLinearSrgb/fromLinearSrgb,并用layout(color)标注真正表示颜色的 uniform。最后,用sksl-minify在发布前压缩你的效果代码,即可在保持渲染结果一致的同时显著减小体积。文中所涉 API 均为 Skia 公开接口,可进一步阅读 include/effects/SkRuntimeEffect.h 与 site/docs/user/color.md、site/docs/user/coordinates.md 获取更完整的细节。

  • 图形学

【免费下载链接】skia

Skia is a complete 2D graphic library for drawing Text, Geometries, and Images. See documentation for contribution instructions.

项目地址:https://gitcode.com/gh_mirrors/ski/skia
点击查看免费下载

相关推荐

上一篇:AndroidVideoCache混淆配置终极指南:确保release版本正常工作的10个关键步骤
下一篇:Loop macOS 窗口管理教程:3 步搞定第一个屏幕分屏布局

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 14:28:10

SDR++ 软件定义无线电实战手册:从一根 SDR 棒到第一路 FM 广播

SDR 软件定义无线电实战手册&#xff1a;从一根 SDR 棒到第一路 FM 广播 【免费下载链接】SDRPlusPlus Cross-Platform SDR Software 项目地址: https://gitcode.com/GitHub_Trending/sd/SDRPlusPlus 如果你一直想在电脑里调出 FM 电台&#xff0c;亲眼看信号强度在瀑布…

作者头像 李华
网站建设 2026/9/24 14:27:19

新能源汽车动力蓄电池仿真教学系统技术深度解析:架构、建模与实训全链路实现

在新能源汽车职业教育数字化转型进程中&#xff0c;动力蓄电池实训始终是核心难点环节&#xff1a;实车设备采购与维护成本高昂、高压操作存在固有安全风险、内部结构与工作原理抽象难懂、实训考核与管理数字化程度低&#xff0c;传统实训模式难以兼顾标准化、规模化与教学质量…

作者头像 李华
网站建设 2026/9/24 14:24:04

嘉立创EDA元件库一键导出Altium Designer:Type-C封装导入实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华