Published on

PixiJS 源码揭秘 - 9. 揭秘Filters与Blend Modes

Authors
  • avatar
    Name
    青雲
    Twitter

目录

在PixiJS中,滤镜(Filters)和混合模式(Blend Modes)是实现丰富视觉效果的强大工具。本文将深入源码,解析它们的工作原理和实现细节。

滤镜基础 (Filters)

滤镜可以为显示对象(DisplayObject)添加各种视觉效果,例如模糊、颜色调整等。它们通过WebGL/WebGPU着色器实现,对图像进行后处理。

Filter 基类

PixiJS中的所有滤镜都继承自 Filter 基类 (src/filters/Filter.ts)。这个基类定义了滤镜的基本接口和通用功能。

// 位置: src/filters/Filter.ts
export class Filter extends GlProgram {
  // ...
}

使用滤镜

在PixiJS中应用滤镜非常直观 (示例来自 Filter.ts:22-32):

// 单个滤镜
sprite.filters = new BlurFilter({ strength: 8 })

// 多个滤镜
sprite.filters = [new BlurFilter({ strength: 8 }), new HardMixBlend()]

当为一个显示对象设置 filters 属性时,可以传入单个滤镜实例或一个滤镜实例数组。

滤镜选项 (FilterOptions)

Filter 类及其子类接受一个可选的 options 对象,用于配置滤镜的行为。接口定义如下 (源自 Filter.ts:66-105):

// 位置: src/filters/Filter.ts
export interface FilterOptions
{
    /**
     * The blend mode of the filter.
     * @default PIXI.BLEND_MODES.NORMAL
     */
    blendMode?: BLEND_MODES;
    /**
     * The resolution of the filter. Setting this to be lower will improve performance,
     * but degrade the quality.
     * @default \'inherit\'
     */
    resolution?: number | \'inherit\';
    /**
     * The padding of the filter. This is the amount of space that will be added to the
     * filter texture.
     * @default 0
     */
    padding?: number;
    /**
     * The antialias of the filter.
     * @default \'inherit\'
     */
    antialias?: FilterAntialias | boolean;
    /**
     * If the filter needs to be blended, this will be true.
     * @default false
     */
    blendRequired?: boolean;
    /**
     * If the filter is an effect that clips to the viewport.
     * @default false
     */
    clipToViewport?: boolean;
}
  • blendMode: 指定滤镜输出与背景混合的模式。
  • resolution: 滤镜渲染时使用的分辨率。可以设置为具体数值或 'inherit' 来继承渲染器分辨率。较低的分辨率可以提升性能但会降低质量。
  • padding: 滤镜效果可能超出原始对象边界,padding 用于扩展渲染纹理的尺寸以容纳这些效果。
  • antialias: 控制滤镜渲染时的抗锯齿效果。
  • blendRequired: 指示该滤镜是否需要特殊的混合操作,通常由滤镜内部自动管理。
  • clipToViewport: 是否将滤镜效果裁剪到视口范围。

实际应用场景

1. 游戏特效

// 受伤效果
function applyHurtEffect(sprite) {
  const colorMatrix = new ColorMatrixFilter()
  colorMatrix.desaturate()
  colorMatrix.tint(0xff0000, 0.5)

  const blur = new BlurFilter(2)

  sprite.filters = [colorMatrix, blur]

  // 2秒后移除效果
  setTimeout(() => {
    sprite.filters = []
  }, 2000)
}

2. UI 效果

// 按钮悬停效果
button.on('pointerover', () => {
  button.filters = [
    new GlowFilter({
      distance: 15,
      outerStrength: 2,
      innerStrength: 0,
      color: 0x00ffff,
      quality: 0.5,
    }),
  ]
})

button.on('pointerout', () => {
  button.filters = []
})

3. 图片处理

// 老照片效果
function applyVintageEffect(sprite) {
  const sepia = new ColorMatrixFilter()
  sepia.sepia(true)

  const noise = new NoiseFilter(0.1)

  const vignette = new VignetteFilter({
    darkness: 0.5,
    offset: 0.2,
  })

  sprite.filters = [sepia, noise, vignette]
}

滤镜的工作原理

当一个或多个滤镜应用于显示对象时,PixiJS的滤镜系统(FilterSystem)会执行以下步骤 (参考 Filter.ts:128-145):

  1. 打断当前渲染批次 (Break Current Render Batch): 滤镜的应用通常需要改变渲染状态,因此会中断当前的批处理渲染。
  2. 测量目标对象尺寸 (Measure Target Bounds): 计算应用滤镜的目标对象的边界框(bounds)。
  3. 获取渲染纹理 (Get Render Texture): 从纹理池(TexturePool)获取合适大小的渲染纹理。
  4. 渲染目标到纹理 (Render Target to Texture): 将目标显示对象渲染到该获取到的临时纹理上。
  5. 应用滤镜着色器 (Apply Filter Shader): 使用滤镜自带的着色器程序,将上一步渲染得到的纹理作为输入,进行处理,并将结果渲染回主帧缓冲区(或父滤镜的输入纹理,或另一个临时纹理)。

多滤镜的"翻转" (Ping-Pong) 技术

当应用多个滤镜时,FilterSystem 会采用一种称为"翻转"(Ping-Pong)的技术。它使用两个临时纹理,轮流作为输入和输出。以下是 FilterSystem.ts:383-405 中相关逻辑的简化表示:

// 简化自 FilterSystem.ts:383-405
let flip = filterData.inputTexture // 初始输入纹理 (渲染了原始对象)
let flop = TexturePool.getOptimalTexture(
  bounds.width,
  bounds.height,
  flip.source._resolution,
  false
)

let i = 0

// 循环应用滤镜
for (i = 0; i < filters.length - 1; ++i) {
  const filter = filters[i]
  filter.apply(this, flip, flop, true) // `this` 指代 FilterSystem 实例
  const t = flip
  flip = flop
  flop = t
}

// 最后一个滤镜直接渲染到输出目标
filters[i].apply(this, flip, filterData.outputTexture, clearMode)
TexturePool.returnTexture(flop)

这种交替使用纹理的方式,使得每个滤镜都能在前一个滤镜处理结果的基础上进行操作,最终得到所有滤镜叠加的效果。

下面是滤镜工作流程的图示:

混合模式 (Blend Modes)

混合模式定义了当一个图像(源)绘制到另一个图像(目标)上时,两者颜色如何组合。

PixiJS 支持多种混合模式,定义在 src/scene/graphics/const.ts (源自 const.ts:5-38):

export type BLEND_MODES =
  | 'inherit'
  | 'normal'
  | 'add'
  | 'multiply'
  | 'screen'
  | 'darken'
  | 'lighten'
  | 'erase'
  | 'color-dodge'
  | 'color-burn'
  | 'linear-burn'
  | 'linear-dodge'
  | 'linear-light'
  | 'hard-light'
  | 'soft-light'
  | 'pin-light'
  | 'difference'
  | 'exclusion'
  | 'overlay'
  | 'saturation'
  | 'color'
  | 'luminosity'
  | 'normal-npm' // normal pre-multiplied alpha
  | 'add-npm' // add pre-multiplied alpha
  | 'screen-npm' // screen pre-multiplied alpha
  | 'none' // no blend mode
  | 'subtract'
  | 'divide'
  | 'vivid-light'
  | 'hard-mix'
  | 'negation'
  | 'min'
  | 'max'

混合模式的分类

PixiJS中的混合模式主要分为两类:

  1. 基本混合模式 (Basic Blend Modes): 这些是WebGL/WebGPU原生支持的混合模式,通过图形API提供的混合函数(如 gl.blendFunc, gl.blendEquation)直接实现。例如 NORMAL, ADD, MULTIPLY, SCREEN 等。这些模式通常性能较好,因为它们是硬件加速的。
  2. 高级混合模式 (Advanced Blend Modes): 这些混合模式在WebGL/WebGPU中没有直接对应的原生函数。PixiJS通过特殊的滤镜来实现它们。例如 OVERLAY, DARKEN, LIGHTEN, COLOR_DODGE, COLOR_BURN 等。由于需要额外的滤镜处理步骤(渲染到纹理,应用着色器),高级混合模式通常比基本混合模式有更高的性能开销。

基本混合模式的实现

基本混合模式的切换由渲染器的状态管理系统处理。

WebGL 实现 (源自 GlStateSystem.ts:284-320):
当设置混合模式时,GlStateSystem 会根据预定义的映射表 (如 mapWebGLBlendModesToPixi.ts) 来调用 gl.blendFuncSeparategl.blendEquationSeparate

 简化自 GlStateSystem.ts setBlendMode 方法
 setBlendMode(value: BLEND_MODES): void {
     if (!this.blendModesMap[value]) {
         value = 'normal'; // 回退到 normal
     }
     if (value === this.blendMode) return;
     this.blendMode = value;
     const mode = this.blendModesMap[value];
     const gl = this.gl;

     if (mode.length === 2) { // [srcFactor, dstFactor]
          gl.blendFunc(mode[0], mode[1]);
     } else { // [srcRGB, dstRGB, srcAlpha, dstAlpha]
         gl.blendFuncSeparate(mode[0], mode[1], mode[2], mode[3]);
     }

     if (mode.length === 6) { // ..., eqRGB, eqAlpha]
         this._blendEq = true;
         gl.blendEquationSeparate(mode[4], mode[5]);
     } else if (this._blendEq) {
         this._blendEq = false;
         gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD); // 默认 equation
     }
 }

WebGL混合模式映射示例 (源自 mapWebGLBlendModesToPixi.ts:17-20):

blendMap.normal = [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]
blendMap.add = [gl.ONE, gl.ONE]
blendMap.multiply = [gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA]
blendMap.screen = [gl.ONE, gl.ONE_MINUS_SRC_COLOR, gl.ONE, gl.ONE_MINUS_SRC_ALPHA]

WebGPU 实现 (源自 GpuBlendModesToPixi.ts:5-16):
在WebGPU中,混合模式定义更为结构化:

GpuBlendModesToPixi.normal = {
  alpha: {
    srcFactor: 'one',
    dstFactor: 'one-minus-src-alpha',
    operation: 'add',
  },
  color: {
    srcFactor: 'one',
    dstFactor: 'one-minus-src-alpha',
    operation: 'add',
  },
}

高级混合模式的实现

高级混合模式通过特殊的滤镜实现,这些滤镜通常继承自 BlendModeFilter

BlendModeFilter 基类 (源自 BlendModeFilter.ts:23-67):

export class BlendModeFilter extends Filter {
  constructor(options: BlendModeFilterOptions) {
    // ... 编译着色器代码 ...
    super({
      gpuProgram, // WebGPU 程序
      glProgram, // WebGL 程序
      blendRequired: true, // 指示此滤镜需要混合
      resources: {
        blendUniforms: uniformGroup,
        uBackTexture: Texture.EMPTY, // 用于背景纹理的 uniform
      },
    })
  }
}

OverlayBlend 为例 (源自 OverlayBlend.ts:15-71):
这个滤镜定义了 GLSL (以及对应的 WGSL) 代码来实现叠加混合算法。

export class OverlayBlend extends BlendModeFilter {
  constructor() {
    super({
      gl: {
        functions: `
                 float overlay(float base, float blend)
                 {
                     return (base < 0.5) ? (2.0*base*blend) : (1.0-2.0*(1.0-base)*(1.0-blend));
                 }

                 vec3 blendOverlay(vec3 base, vec3 blend, float opacity)
                 {
                     vec3 blended = vec3(
                         overlay(base.r, blend.r),
                         overlay(base.g, blend.g),
                         overlay(base.b, blend.b)
                     );
                     return (blended * opacity + base * (1.0 - opacity));
                 }
                 `,
        main: `
                 finalColor = vec4(blendOverlay(back.rgb, front.rgb,front.a), blendedAlpha) * uBlend;
                 `,
      },
      // ... WebGPU 实现类似 ...
    })
  }
}

高级混合模式由 BlendModePipe (位于 src/rendering/renderers/shared/blendModes/BlendModePipe.ts) 管理。当一个显示对象的 blendMode 被设置为高级混合模式时:

  1. BlendModePipe 检测到这是一个高级混合模式。
  2. 它会从一个内部映射 BLEND_MODE_FILTERS 中查找对应的滤镜类。
// 位置: src/rendering/renderers/shared/blendModes/BlendModePipe.ts
const BLEND_MODE_FILTERS: Partial<Record<BLEND_MODES, new () => BlendModeFilter>> = {} as const

extensions.handle(ExtensionType.BlendMode, (value) => {
  // ... 注册高级混合模式对应的滤镜类
  BLEND_MODE_FILTERS[value.name as BLEND_MODES] = value.ref
})
  1. BlendModePipesetBlendMode 方法会处理切换 (源自 BlendModePipe.ts:48-113 的简化逻辑):
 setBlendMode(renderable: Renderable, blendMode: BLEND_MODES, instructionSet: InstructionSet) {
     if (this._activeBlendMode === blendMode) {
         if (this._isAdvanced) this._renderableList.push(renderable);
         return;
     }

     this._activeBlendMode = blendMode;

     if (this._isAdvancedPreviously) { // 如果上一个是高级混合,则结束它
         this._endAdvancedBlendMode(instructionSet);
     }

     this._isAdvanced = !!BLEND_MODE_FILTERS[blendMode]; // 当前是否是高级混合

     if (this._isAdvanced) {
         this._beginAdvancedBlendMode(instructionSet, blendMode); // 开始新的高级混合
         this._renderableList.push(renderable);
     }
     // 如果不是高级混合,渲染器会处理基本混合模式
 }
  1. _beginAdvancedBlendMode 方法中 (源自 BlendModePipe.ts:115-150), 它会创建一个 FilterEffect 实例,并将对应的高级混合模式滤镜添加到这个 FilterEffect 中。然后将这个 FilterEffect 推入滤镜系统的渲染指令中。
 private _beginAdvancedBlendMode(instructionSet: InstructionSet, blendMode: BLEND_MODES) {
     // ...
     let filterEffect = this._filterHash[blendMode];

     if (!filterEffect) {
         filterEffect = this._filterHash[blendMode] = new FilterEffect();
         // 获取对应的滤镜构造函数并实例化
         filterEffect.filters = [new BLEND_MODE_FILTERS[blendMode as keyof typeof BLEND_MODE_FILTERS]()];
     }

     const instruction: FilterInstruction = { // 创建滤镜指令
         renderPipeId: 'filter',
         action: 'pushFilter', // 推入滤镜
         renderables: [], // 将受此滤镜影响的渲染对象列表
         filterEffect,
         canBundle: false,
     };

     this._renderableList = instruction.renderables; // 后续对象会被加入此列表
     instructionSet.add(instruction); // 添加到指令集
 }
  1. 当混合模式需要切换回基本模式或不再应用时 (_endAdvancedBlendMode),BlendModePipe 会执行 popFilter 相关的操作,移除该滤镜效果。

高级混合模式的滤镜本身通常是一个简单的片段着色器,它接收两个纹理作为输入(通常是当前渲染对象的纹理和帧缓冲区的当前内容,通过 uBackTexture 访问背景),并根据特定的混合算法计算输出颜色。

混合模式分类图示:

性能考量

使用滤镜和高级混合模式会带来一定的性能开销:

  • 额外的渲染操作: 每个滤镜(包括用于高级混合模式的滤镜)通常需要至少一次额外的绘制调用(draw call)和纹理操作(渲染到纹理)。
  • 纹理切换: 频繁地在渲染目标之间切换(Framebuffer Blits 或 Copies)也可能消耗性能。
  • 着色器复杂度: 复杂的滤镜着色器会增加GPU的计算负担。
  • 填充率 (Fill Rate): 对于全屏滤镜或覆盖大面积的滤镜,GPU的像素填充率可能成为瓶颈。

为了获得更好的性能:

  • 减少滤镜数量: 尽量合并或减少滤镜的使用。每个滤镜都会增加开销。
  • 降低滤镜分辨率: 通过设置滤镜的 resolution 选项,可以降低用于滤镜效果的临时纹理的分辨率,从而减少像素处理量,但这可能会牺牲一些视觉质量。
  • 对容器应用滤镜: 如果多个相邻的对象需要相同的滤镜效果,将它们放入一个容器(Container)中,然后对该容器应用单个滤镜,通常比对每个对象单独应用滤镜效率更高。这是因为只需要进行一次"渲染到纹理"的操作,而不是多次 (参考 Filter.ts:126-145 中的设计理念)。

性能优化建议

1. 合理设置分辨率

通过调整 resolution 参数可以在质量和性能之间取得平衡:

// 高质量(默认)
const highQualityFilter = new BlurFilter({ resolution: 2 })

// 平衡模式
const balancedFilter = new BlurFilter({ resolution: 1 })

// 性能优先
const performanceFilter = new BlurFilter({ resolution: 0.5 })

建议

  • 移动设备上可以适当降低分辨率
  • 对于大尺寸显示对象,可以设置较低的分辨率
  • 静态内容可以缓存渲染结果

2. 优化滤镜链

多个滤镜会按顺序执行,每个滤镜都会产生额外的绘制调用:

// 不推荐的写法
sprite.filters = [
  new BlurFilter({ strength: 4 }),
  new ColorMatrixFilter().sepia(true),
  new NoiseFilter(0.2),
]

// 推荐的优化方式
// 1. 合并相似的效果
// 2. 考虑使用着色器直接实现组合效果
class CustomEffect extends Filter {
  constructor() {
    super(/* 自定义着色器 */)
  }
}

3. 合理使用 padding

// 默认情况下,PixiJS 会自动计算 padding
const defaultPadding = new BlurFilter({ strength: 8 })

// 手动设置 padding 可以优化性能
const optimizedPadding = new BlurFilter({
  strength: 8,
  padding: 16, // 根据实际效果需求调整
})

4. 缓存策略

对于不经常变化的内容,考虑使用 CacheAsBitmap

sprite.filters = [new BlurFilter()]
sprite.cacheAsBitmap = true // 缓存渲染结果

// 当需要更新时
sprite.cacheAsBitmap = false
// 更新内容...
sprite.cacheAsBitmap = true

5. 避免在动画中频繁创建/销毁滤镜

// 不推荐
function animate() {
  // 每帧都创建新实例
  sprite.filters = [new BlurFilter({ strength: Math.random() * 10 })]
}

// 推荐
const filter = new BlurFilter()
sprite.filters = [filter]

function animate() {
  // 只更新属性
  filter.blur = Math.random() * 10
}

注意事项

  • 嵌套混合: 嵌套使用需要混合的滤镜(即滤镜本身设置了 blendMode 属性)可能不会按预期工作,或者导致复杂的渲染路径。
  • 渲染组 (Render Groups): 渲染组(一种高级优化技术,较少使用)与滤镜一起使用时可能存在兼容性问题或未定义行为。
  • 频繁开关滤镜: 如果需要频繁地启用和禁用滤镜,建议修改滤镜实例的 enabled 属性,而不是从显示对象的 filters 数组中添加或移除滤镜。直接修改 enabled 属性通常更高效,因为它避免了重新分配数组和可能的内部状态重建 (参考 FilterSystem.ts:42-60 中对 enabled 属性的处理逻辑)。
// 推荐的方式
myFilter.enabled = false // 禁用
myFilter.enabled = true // 启用

// 不太推荐的方式 (如果频繁操作)
sprite.filters = [] // 移除
sprite.filters = [myFilter] // 添加

FilterSystem 在处理滤镜时会检查 enabled 属性。

总结

PixiJS的滤镜和混合模式系统提供了一套灵活且强大的API,用于创建引人入胜的视觉效果。理解其工作原理,特别是基本混合模式与通过滤镜实现的高级混合模式之间的区别,以及相关的性能影响,对于有效地使用这些功能至关重要。通过合理地组织场景图和明智地选择滤镜参数,开发者可以在视觉丰富性和应用性能之间取得良好的平衡。