Skia SkBlendMode 混合模式全解析:从 Porter-Duff 到非分离混合的完整指南
【免费下载链接】skiaSkia is a complete 2D graphic library for drawing Text, Geometries, and Images. See documentation for contribution instructions.项目地址: https://gitcode.com/gh_mirrors/ski/skia
SkBlendMode 是 Skia 2D 图形库中定义"源像素如何与目标像素合成"的核心枚举类型,它决定了每次draw操作中源(source)颜色与画布上已有目标(destination)颜色之间的混合规则。本文以 site/docs/user/api/SkBlendMode_Overview.md 文档为主体骨架,结合 Skia 源码(include/core/SkBlendMode.h、src/core/SkBlendMode.cpp)与测试用例,系统讲解全部 29 种混合模式的数学原理、分类体系、值域边界与工程实现,帮助你准确选用混合模式并理解其在光栅管线中的真实行为。
SkBlendMode 是什么
SkBlendMode 描述的是:目标像素如何被"自身与源像素的组合"所替换。从 SkBlendMode 头文件 的注释可以看到,混合(Blend)是接收两个颜色(源 source、目标 destination)并返回一个新颜色的算子。其核心特征包括:
- 逐分量独立运算:绝大多数混合模式对红、绿、蓝、Alpha 四个分量执行相同的运算规则,因此文档与实现只需描述单个分量的行为,而非逐一命名每个分量;
- 不使用相邻像素:每个目标像素的最终结果仅由该位置上的源像素与目标像素决定,与周围像素无关;
- Alpha 参与合成:源与目标各自的 Alpha 值共同决定写入目标像素的 Alpha;同时,两者 Alpha 也可能影响写入的目标颜色分量。
源码注释还给出了文档采用的数学约定:分量值一律归一化表述为 0..1 的浮点数,并使用缩写s(source)、d(destination)、sa(source alpha)、da(destination alpha),结果缩写r(四分量同规则)、ra(结果 Alpha)、rc(结果颜色分量)。
值域约定与两个重要例外
无论源像素与目标像素中的 Alpha 采用何种编码方式,几乎所有 SkColorType 都将 Alpha 视为 0 到 1 之间的值;同样地,几乎所有混合模式的算法都会把输出结果限制在 0 到 1 的范围内。这一约定保证了混合结果的数学良定义性。
但存在两个例外:
| 例外 | 行为说明 |
|---|---|
SkBlendMode::kPlus | 允许计算出的 Alpha 与颜色分量值大于 1。对于除kRGBA_F16_SkColorType之外的色彩类型,结果 Alpha 与分量值会被钳制到 1; |
kRGBA_F16_SkColorType | 允许数值超出 0..1 范围,因此是否将结果控制在 0..1 内完全由客户端负责,只有保证结果落在该区间内,混合结果才是良定义的。 |
从实现看,kPlus 的钳制行为体现在 src/core/SkBlendMode.cpp 的SkBlendMode_ShouldPreScaleCoverage注释中:Plus 模式"总是需要预缩放",这是其 SkRasterPipeline 实现的特定行为,目的是把钳制放进混合模式自身,而不是在 lerp 之后追加独立的 clamp 阶段。而在 include/core/SkBlendMode.h#L51 中,kPlus 的公式被写作r = min(s + d, 1),正是"加后钳制到 1"的数学表达。
Porter-Duff 混合模式:kClear 到 kXor
Porter-Duff 混合模式组覆盖SkBlendMode::kClear到SkBlendMode::kXor共 12 种,其理论来源是经典论文Compositing Digital Images(Porter 与 Duff 发表于 1984 年)。这些模式对 Alpha 与颜色分量采用统一的系数加权形式:结果 = src 系数 × src + dst 系数 × dst。
从 include/core/SkBlendMode.h#L38-L74 的枚举定义,可以提取出每种模式的完整公式:
| 模式 | 公式(r 表示四分量同规则) | 语义 |
|---|---|---|
kClear | r = 0 | 清除目标 |
kSrc | r = s | 直接用源覆盖 |
kDst | r = d | 保留目标 |
kSrcOver | r = s + (1-sa)*d | 源叠加于目标之上(默认模式) |
kDstOver | r = d + (1-da)*s | 目标叠加于源之上 |
kSrcIn | r = s * da | 源仅保留在目标覆盖区域 |
kDstIn | r = d * sa | 目标仅保留在源覆盖区域 |
kSrcOut | r = s * (1-da) | 源仅保留在目标之外的区域 |
kDstOut | r = d * (1-sa) | 目标仅保留在源之外的区域 |
kSrcATop | r = s*da + d*(1-sa) | 目标覆盖源的部分 |
kDstATop | r = d*sa + s*(1-da) | 源覆盖目标的部分 |
kXor | r = s*(1-da) + d*(1-sa) | 互斥区域保留 |
系数化实现的深层原理
Skia 在 src/core/SkBlendMode.cpp#L58-L96 的SkBlendMode_AsCoeff中,为这 15 种模式(kClear 到 kScreen,即mode <= kLastCoeffMode)建立了系数表。表中每个模式对应一对SkBlendModeCoeff枚举值(源系数与目标系数),系数可以是常量、Alpha 函数或颜色函数:
CoeffRec{ SkBlendModeCoeff::kZero, SkBlendModeCoeff::kZero }, // clear CoeffRec{ SkBlendModeCoeff::kOne, SkBlendModeCoeff::kISA }, // src-over CoeffRec{ SkBlendModeCoeff::kDA, SkBlendModeCoeff::kISA }, // src-atop CoeffRec{ SkBlendModeCoeff::kOne, SkBlendModeCoeff::kISC }, // screen其中SkBlendModeCoeff枚举定义于 include/core/SkBlendMode.h#L84-L97,包括kZero、kOne、kSC(源颜色)、kISC(1 - 源颜色)、kDC、kIDC、kSA、kISA、kDA、kIDA十种系数。例如 kSrcOver 的系数对为(kOne, kISA),展开即1×s + (1-sa)×d,与头文件注释完全一致。这种系数化抽象让 GPU 后端(如 src/gpu/ganesh/GrXferProcessor.cpp)可以统一生成混合着色器,而非为每个模式写死代码。
透明绘制行为的差异
文档特别指出两种 Porter-Duff 应用场景的差异:
- 绘制带透明度的位图(bitmap)时,Porter-Duff 合成是"自由"的,允许清除目标像素——透明区域可以真正把目标擦掉;
- 绘制带透明度的几何图形时,透明的源像素不会参与组合,因此几何图形外部的目标像素保持原样、不被触碰。
这一点与 Skia 的混合分类相关:kLastCoeffMode = kScreen标记了 Porter-Duff 组的末尾(见 include/core/SkBlendMode.h#L71),而kLastSeparableMode = kMultiply与kLastMode = kLuminosity则分别划分出可分离与非分离混合组的边界,kSkBlendModeCount因此等于 29。
明暗调整模式:从简单算术到复杂算法
模式组kPlus与kScreen使用简单的算术来提亮或压暗目标:
kPlus:r = min(s + d, 1)—— 直接相加后钳制;kScreen:r = s + d - s*d—— 经典的屏幕混合,结果总是比单独任一方更亮。
而kOverlay到kMultiply这一组则使用更复杂的算法,甚至同一个模式会同时具备"变亮"与"变暗"两种行为(取决于目标与源的明暗关系),这正是 Blend Modes 中定义的通用 Photoshop 风格混合模式:
| 模式 | 行为 |
|---|---|
kOverlay | 依据目标明暗选择 multiply 或 screen(multiply or screen, depending on destination) |
kDarken | rc = s + d - max(s*da, d*sa),Alpha 按 kSrcOver 规则 |
kLighten | rc = s + d - min(s*da, d*sa),Alpha 按 kSrcOver 规则 |
kColorDodge | 提亮目标以反映源 |
kColorBurn | 压暗目标以反映源 |
kHardLight | 依据源明暗选择 multiply 或 screen |
kSoftLight | 依据源明暗选择变亮或变暗 |
kDifference | rc = s + d - 2*(min(s*da, d*sa)) |
kExclusion | rc = s + d - two(s*d) |
kMultiply | r = s*(1-da) + d*(1-sa) + s*d |
其中kMultiply的公式值得注意:它不是简单的s*d,而是对源与目标各自未覆盖的部分做了 Alpha 加权,再叠加乘积项,因此效果等价于"非预乘值相乘"(详见下节 kModulate 对比)。
kModulate:kSrcATop 与 kMultiply 的结合体
kModulate是kSrcATop与kMultiply的"混血"模式,其核心差异在于:
- kModulate 对所有分量(包括 Alpha)做乘法,公式即
r = s*d。因此只要源或目标中有一个是透明的,结果就是透明的; - kModulate 使用预乘(Premultiplied)值计算乘积,而
kMultiply使用非预乘(Unpremultiplied)值计算乘积。
换句话说:kModulate 在预乘色彩空间中将 RGBA 四通道全部直接相乘,天然保持了"透明即零贡献"的语义;而 kMultiply 的公式r = s*(1-da) + d*(1-sa) + s*d可以看作是在非预乘空间完成的乘法,并显式处理了两者 Alpha 的补偿项。这一点在 include/core/SkBlendMode.h#L52 中直接体现:kModulate, //!< r = s*d。
有趣的是,从 Skia 的系数表看(src/core/SkBlendMode.cpp#L82),kModulate 的系数对为(kZero, kSC),即0×s + sc×d,恰好是"以源颜色缩放目标"的线性表述;而 kScreen 的系数对为(kOne, kISC),对应s + (1-s)×d。这解释了为什么 kModulate 可以归类到系数模式组(<= kLastCoeffMode),而 kMultiply 必须归入可分离模式组(<= kLastSeparableMode)。
非分离颜色混合:kHue、kSaturation、kColor、kLuminosity
最后一组模式kHue、kSaturation、kColor、kLuminosity与其他所有模式都不同:它们不能对四个分量独立运算,而是必须使用全部颜色分量信息、在 HSL/HSV 类色彩空间中对源与目标进行重组,属于 W3C Compositing and Blending 规范定义的non-separable blend modes(非分离混合模式):
| 模式 | 语义 |
|---|---|
kHue | 使用源色相 + 目标的饱和度与明度 |
kSaturation | 使用源饱和度 + 目标的色相与明度 |
kColor | 使用源的色相与饱和度 + 目标的明度 |
kLuminosity | 使用源的明度 + 目标的色相与饱和度 |
由于这类模式需要把 RGB 转换到包含色相(Hue)、饱和度(Saturation)的中间色彩空间再重组,其实现无法用简单的线性系数表达,因此它们被排除在系数模式与可分离模式之外,仅能通过kLastMode = kLuminosity之前的枚举序号标识。在光栅管线中它们对应独立的hue、saturation、color、luminosity阶段(见下文)。
在代码中使用 SkBlendMode
通过 SkPaint 设置混合模式
最常见的用法是通过SkPaint::setBlendMode(SkBlendMode mode)设置绘制时的混合模式,定义见 include/core/SkPaint.h#L470-L474。Skia 同时提供配套的查询接口:
asBlendMode():若当前 blender 可表示为SkBlendMode枚举则返回其值,否则返回空 optional(include/core/SkPaint.h#L456);getBlendMode_or(SkBlendMode defaultMode):无法表示时返回调用方提供的默认模式(include/core/SkPaint.h#L462);isSrcOver():判断当前 blender 是否等价于默认的 kSrcOver 行为(include/core/SkPaint.h#L468);setBlender()/getBlender()/refBlender():更通用的自定义混合函数接口,setBlendMode本质上是"设置一个实现了指定混合模式枚举的 SkBlender"的便捷方法(include/core/SkPaint.h#L494-L499)。当 blender 为 nullptr 时,一律表示默认的 SrcOver 行为。
一个最小示例(参考 tests/BlendTest.cpp 的测试写法):
SkPaint paint; paint.setColor(SkColorSetRGB(0x80, 0xFF, 0xFF)); paint.setBlendMode(SkBlendMode::kMultiply); // 使用乘色混合 canvas->drawRect(SkRect::MakeXYWH(2, 2, 5, 5), paint);调试辅助:SkBlendMode_Name
若要打印或序列化混合模式名称,可调用SkBlendMode_Name(SkBlendMode blendMode),其实现见 src/core/SkBlendMode.cpp#L167-L202,返回 "Clear"、"SrcOver"、"Multiply" 等与枚举名对应的 C 字符串。
底层实现:光栅管线中的阶段映射
SkBlendMode 在软件渲染路径中由 SkRasterPipeline 执行。函数SkBlendMode_AppendStages(src/core/SkBlendMode.cpp#L98-L134)将每个枚举映射到一个光栅管线阶段(stage):
case SkBlendMode::kSrcOver: p->append(SkRasterPipelineOp::srcover); return; case SkBlendMode::kModulate: p->append(SkRasterPipelineOp::modulate); return; case SkBlendMode::kMultiply: p->append(SkRasterPipelineOp::multiply); return; case SkBlendMode::kHue: p->append(SkRasterPipelineOp::hue); return;值得关注的工程细节:
kSrc被映射为no-op(源即结果,无需任何运算);kDst被映射为move_dst_src(把目标拷贝到源位置);- 未知/非法模式兜底为
srcover。
在 src/core/SkBlendMode.cpp#L136-L165 的SkBlendMode_Apply中,Skia 还针对kClear、kSrc、kDst、kSrcOver四种高频模式做了特判快路径(直接返回常量或单条 SIMD 表达式src + dst*(1-sa)),其余模式才走完整的光栅管线流水:加载目标 → 交换源/目标 → 加载源 → 追加混合阶段 → 存储结果。
此外,CheckFastPath(src/core/SkBlendMode.cpp#L208-L241)在绘制层面进一步优化:例如kDst可直接跳过绘制(kSkipDrawing);当目标不透明且画笔为纯色时,kSrcIn可退化为kSrcOver;kDstOver在目标不透明时同样可跳过。这些优化确保最常见的混合场景不会付出无谓的渲染代价。
测试验证:混合正确性有据可查
Skia 对混合模式有系统性的测试保障:
- tests/BlendTest.cpp#L64-L89 的
Blend_byte_multiply测试专门验证 8-bit 整数乘法的舍入精度:穷举 256×256 全组合,要求结果与理想值(x*y+127)/255的偏差不超过 1,且透明必须保持透明(diffs_0x00 == 0)、不透明必须保持不透明(diffs_0xff == 0)。测试还指出(x*y)>>8、(x*y+128)>>8等"看似正确实则损坏"的实现,只有(x*y+127)/255及少数等价变体才是完美方案——这正是 kMultiply 在整数色彩类型下舍入行为的工程约束; - tests/BlendTest.cpp#L92-L189 的
ES2BlendWithNoTexture测试在 GL 上下文中验证 kMultiply 混合结果,确认矩形内部像素按混合公式变为0x80FF80,而混合区域之外保持目标原色0xFFFF80; - tests/BlendTest.cpp#L194-L241 的
BlendRequiringDstReadWithLargeCoordinates测试针对需要读取目标的混合模式(如 kSoftLight)在大坐标下的正确性回归(skbug.com/40045423)。
此外,tests/GrPorterDuffTest.cpp、tests/AdvancedBlendTest.cpp 等测试文件分别覆盖 Porter-Duff 系数模式与高级混合模式在 GPU 后端的行为。
小结
SkBlendMode 的 29 种模式可按三组理解:Porter-Duff 系数模式(kClear–kScreen,含 kPlus、kModulate)以线性系数加权为核心,可直接映射为 GPU 固定混合方程;可分离模式(kOverlay–kMultiply)逐分量使用非线性算法;非分离模式(kHue–kLuminosity)则需在色彩空间中整体重组。理解其值域约定(0..1 归一化、kPlus 钳制、kRGBA_F16 例外)与源码中的系数表、光栅管线阶段映射及快速路径优化,能帮助你在选择混合模式时兼顾视觉效果与渲染性能,并准确判断各类色彩类型下的数值行为。
【免费下载链接】skiaSkia is a complete 2D graphic library for drawing Text, Geometries, and Images. See documentation for contribution instructions.项目地址: https://gitcode.com/gh_mirrors/ski/skia
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考