- Published on
PixiJS 源码揭秘 - 9. 揭秘Filters与Blend Modes
- Authors
- Name
- 青雲
目录
在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
):
- 打断当前渲染批次 (Break Current Render Batch): 滤镜的应用通常需要改变渲染状态,因此会中断当前的批处理渲染。
- 测量目标对象尺寸 (Measure Target Bounds): 计算应用滤镜的目标对象的边界框(bounds)。
- 获取渲染纹理 (Get Render Texture): 从纹理池(
TexturePool
)获取合适大小的渲染纹理。 - 渲染目标到纹理 (Render Target to Texture): 将目标显示对象渲染到该获取到的临时纹理上。
- 应用滤镜着色器 (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中的混合模式主要分为两类:
- 基本混合模式 (Basic Blend Modes): 这些是WebGL/WebGPU原生支持的混合模式,通过图形API提供的混合函数(如
gl.blendFunc
,gl.blendEquation
)直接实现。例如NORMAL
,ADD
,MULTIPLY
,SCREEN
等。这些模式通常性能较好,因为它们是硬件加速的。 - 高级混合模式 (Advanced Blend Modes): 这些混合模式在WebGL/WebGPU中没有直接对应的原生函数。PixiJS通过特殊的滤镜来实现它们。例如
OVERLAY
,DARKEN
,LIGHTEN
,COLOR_DODGE
,COLOR_BURN
等。由于需要额外的滤镜处理步骤(渲染到纹理,应用着色器),高级混合模式通常比基本混合模式有更高的性能开销。
基本混合模式的实现
基本混合模式的切换由渲染器的状态管理系统处理。
WebGL 实现 (源自 GlStateSystem.ts:284-320
):
当设置混合模式时,GlStateSystem
会根据预定义的映射表 (如 mapWebGLBlendModesToPixi.ts
) 来调用 gl.blendFuncSeparate
和 gl.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
被设置为高级混合模式时:
BlendModePipe
检测到这是一个高级混合模式。- 它会从一个内部映射
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
})
BlendModePipe
的setBlendMode
方法会处理切换 (源自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);
}
// 如果不是高级混合,渲染器会处理基本混合模式
}
- 在
_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); // 添加到指令集
}
- 当混合模式需要切换回基本模式或不再应用时 (
_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,用于创建引人入胜的视觉效果。理解其工作原理,特别是基本混合模式与通过滤镜实现的高级混合模式之间的区别,以及相关的性能影响,对于有效地使用这些功能至关重要。通过合理地组织场景图和明智地选择滤镜参数,开发者可以在视觉丰富性和应用性能之间取得良好的平衡。