PixiJS 自定义着色器入门:使用 Filters 实现特效

作者:小圆点

时间:2026-11-03

浏览:70

深入浅出讲解如何在 PixiJS 中通过自定义 Filter 编写 GLSL 片元着色器实现画面后处理特效,涵盖渲染架构、Uniforms 传递、逐帧动画驱动及边缘裁剪等实战避坑经验。

文章目录
PixiJS 渲染技术 主题:GLSL 片元着色器与后处理特效 阅读时间:约 12 分钟
PixiJS 自定义着色器入门:使用 Filters 实现特效

在 PixiJS 开发 2D 互动项目或游戏界面时,开发者经常需要实现如水波扰动、画面融化、全息扫描线、色彩分离(RGB Split)或镜头模糊等视觉特效。虽然 PixiJS 自带了一些常用滤镜,但面对个性化的美术需求,封装好的现成滤镜很快就会捉襟见肘。直接编写自定义着色器(Custom Shader)正是解决这一痛点的根本手段。而在 PixiJS 整个渲染生态中,最优雅、门槛最低的接入方式并不是手动重写几何体绘制流程,而是基于 PIXI.Filter 构建后处理特效。

本文将从 PixiJS Filter 的底层运行机制讲起,带你完整编写一段 GLSL 片元着色器代码,演示如何在 JavaScript 端传递与驱动 Uniforms 参数,并最终把一个动态波纹扭曲特效稳定运行在屏幕上。同时,我们还会重点剖析滤镜边界裁剪、浮点精度陷阱以及帧循环中的性能优化策略。

一、PixiJS Filter 的运行机制与数据流向

很多初学者一听到“着色器”就会联想到繁琐的 WebGL 初始化代码:创建顶点缓存(VBO)、编写投影矩阵、绑定帧缓冲区(FBO)以及设置视口尺寸。PixiJS 的 Filter 类之所以适合入门,是因为它将这些脏活累活全部在底层自动化了。

当给一个精灵或容器设置了 sprite.filters = [customFilter] 时,PixiJS 的渲染管线会发生以下变化:

  1. 离屏渲染捕获:渲染器不会直接将目标对象绘制到主屏幕,而是先将其内容栅格化到一个临时的离屏纹理(RenderTexture)中。
  2. 自动注入标准输入:这个离屏纹理会被自动绑定到名为 uSampler 的 2D 采样器上,同时 PixiJS 会利用其内置的标准顶点着色器,计算好对象在屏幕上的归一化纹理坐标,通过 vTextureCoord 传递给片元着色器。
  3. 片元着色器并行运算:GPU 逐像素运行开发者编写的片元代码,根据 UV 坐标提取 uSampler 上的原图像素,并结合开发者通过 Uniforms 传入的动态参数(如时间、波长、强度)进行变换计算。
  4. 写回屏幕或级联给下一个滤镜:着色计算完成后,像素结果写入目标缓冲区;如果该对象挂载了多个滤镜,上一个滤镜的输出便会成为下一个滤镜的 uSampler,形成流水线式的滤镜链。
PixiJS 自定义着色器入门:使用 Filters 实现特效
图 1:PixiJS Filter 运行架构与管线数据流向示意图

这意味着在 90% 以上的画面后处理场景中,你完全不需要重写顶点着色器(Vertex Shader),只需要聚焦于片元着色器(Fragment Shader)中颜色与采样的算法逻辑,这大大收窄了学习半径。

二、实现自定义 Filter 的核心四步骤

从零搭建一个能跑起来的自定义滤镜,流程非常清晰,主要分为四个紧密相连的环节:编写 GLSL 片元着色器源码、配置 Uniforms 映射字典、实例化 PIXI.Filter 以及挂载至显示节点并驱动更新。

PixiJS 自定义着色器入门:使用 Filters 实现特效
图 2:使用 PixiJS Filter 实现自定义着色器特效的四步全流程

第 1 步:编写 GLSL 片元着色器代码

我们以最实用的“水波扭曲效果”为例。该效果的核心逻辑是通过正弦波对采样的 X 轴纹理坐标进行微小扰动。下面是完整的 GLSL 片元代码,通常以字符串形式存放在 JavaScript 模板字符串中:

// 声明浮点数默认精度(移动端兼容必备)
precision mediump float;

// 由 PixiJS 顶点着色器传递进来的归一化纹理坐标 [0.0, 1.0]
varying vec2 vTextureCoord;

// PixiJS 自动绑定的源图像纹理采样器
uniform sampler2D uSampler;

// 开发者自定义的 Uniform 参数
uniform float uTime;       // 逐帧累加的时间变量
uniform float uFrequency;  // 水波的振动频率
uniform float uAmplitude;  // 水波的扰动幅度

void main() {
    // 复制当前像素的标准 UV 坐标
    vec2 coord = vTextureCoord;

    // 根据 Y 坐标和时间计算正弦偏移量,施加在 X 轴上
    float distortion = sin(coord.y * uFrequency + uTime) * uAmplitude;
    coord.x += distortion;

    // 使用扭曲后的坐标提取原图纹理,并输出最终片元颜色
    gl_FragColor = texture2D(uSampler, coord);
}
GLSL 语法要点提醒

GLSL 是一门强类型语言,不存在 JavaScript 中的隐式类型转换。如果在 GLSL 声明了 float 类型变量,就绝对不能赋值或参与 int 整数运算。例如 coord.x += 1; 会直接引发着色器编译错误,必须写作 coord.x += 1.0;。

第 2 步:定义 Uniforms 映射字典

片元着色器中的 uTime、uFrequency、uAmplitude 是外部变量。在 JavaScript 中,我们需要准备一个普通对象来承载它们的初值。对象中的属性键名必须与 GLSL 代码里的 uniform 变量名完全一致:

const customUniforms = {
    uTime: 0.0,        // 初始时间戳
    uFrequency: 18.0,  // 频率:控制水波条纹的密集程度
    uAmplitude: 0.02   // 振幅:控制波纹扭曲的最大偏移像素比率
};

第 3 步:实例化 PIXI.Filter

在 PixiJS 中,PIXI.Filter 构造函数的标准签名通常接收三个参数:顶点着色器源码、片元着色器源码和 Uniforms 字典。当不需要自定义顶点着色器时,直接将第一个参数传为 undefined 或 null,PixiJS 就会自动挂载默认顶点着色器:

// 实例化自定义滤镜
const waveFilter = new PIXI.Filter(
    undefined,       // 使用默认顶点着色器
    fragmentShader,  // 上述编写的片元着色器字符串
    customUniforms   // 传入 uniforms 初始对象
);

// 设定溢出边距保护(后文会深入讲解其重要性)
waveFilter.padding = 20;

第 4 步:挂载至目标对象并由 Ticker 驱动动画

着色器本身是静态的单次计算。要让波纹动起来,就必须在每一帧渲染前递增 uTime。我们借助 PixiJS 提供的 app.ticker 循环来实现:

// 1. 将滤镜赋值给精灵节点(支持数组形式传入多个滤镜)
bannerSprite.filters = [waveFilter];

// 2. 在帧循环中更新时间,产生连绵不断的波浪效果
app.ticker.add((delta) => {
    // 随帧率平滑累加时间步长
    waveFilter.uniforms.uTime += 0.05 * delta;
});

三、核心算法剖析:UV 坐标扭曲与片元采样细节

为什么仅仅改变了采样坐标就能产生弯曲的视觉效果?理解 UV 空间的坐标映射是掌握所有二维着色器特效的金钥匙。

在 WebGL 中,纹理贴图使用归一化坐标系,左上角或左下角通常为 (0.0, 0.0),对角线终点为 (1.0, 1.0)。默认情况下,屏幕上的每个像素片段与离屏纹理的采样坐标是严格一一对应的。图 3 直观展示了从普通采样到波形位移、再到多通道色彩分离的几何演进过程:

PixiJS 自定义着色器入门:使用 Filters 实现特效
图 3:UV 纹理坐标扭曲与片元采样核心机制解析

1. 正弦位移原理

在公式 coord.x += sin(coord.y * uFrequency + uTime) * uAmplitude; 中:

  • coord.y * uFrequency:使得沿竖直方向不同高度的像素具有不同的三角函数相位,从而形成周期性起伏的条纹波。
  • + uTime:随着时间流逝,正弦曲线整体发生平移,肉眼便观察到了波浪滚滚前进的动画。
  • * uAmplitude:控制最大位移量。如果振幅过大,图像会过度撕裂扭曲;如果振幅适中,就会呈现柔和的水纹效果。

2. 进阶演变:赛博朋克色散(RGB Split)

掌握了单次采样扭曲后,你可以尝试在片元着色器中执行多次不同偏移量的采样,从而实现具有未来感的色相分离特效:

vec2 uv = vTextureCoord;
vec2 offset = vec2(0.008 * sin(uTime * 2.0), 0.0);

// 分别对红色、绿色、蓝色通道执行不同位置的纹理采样
float r = texture2D(uSampler, uv - offset).r;
float g = texture2D(uSampler, uv).g;
float b = texture2D(uSampler, uv + offset).b;
float a = texture2D(uSampler, uv).a;

gl_FragColor = vec4(r, g, b, a);

四、初学者高频踩坑点与排查指南

在着色器开发过程中,控制台往往不会报出常规的 JavaScript 异常,取而代之的是画面变黑、边缘断裂或帧率暴跌。以下梳理了四个最常见的问题及精准解决方法:

故障现象 根本原因分析 针对性解决方案
边缘整齐截断
(波纹外凸处消失)
PixiJS 默认生成的离屏纹理只刚好包裹显示对象的 getBounds() 矩形,位移超出部分被硬性裁剪。 显式设置滤镜外扩边距:waveFilter.padding = 30;,为波纹形变预留充足的像素缓冲空间。
手机/移动端黑屏
(PC 端显示正常)
片元着色器首行未声明 precision mediump float;,移动端 GPU 对精度要求严格,无法推断精度。 在片元着色器代码第一行务必明确声明浮点精度;若需要极高精度计算可声明 highp。
运行一段时间后严重掉帧 在 ticker.add 内部重复执行 new PIXI.Filter() 或每次赋新对象,导致大量 FBO 无法释放,触发频繁 GC。 Filter 必须在循环外部只初始化一次;在 Ticker 中仅修改现有变量属性:filter.uniforms.uTime += 0.02;。
控制台编译报错
(Unknown uniform)
JavaScript 中定义的 key 与片元着色器中的 uniform 变量名称不匹配,或传参类型为非浮点。 严格检查拼写;传递数字时确保为有效数值,绝不能传入 undefined、null 或 NaN。
重要注意:Filter 的性能与 Draw Call 成本

每个挂载了 Filter 的节点都会增加额外的渲染阶段(Render Pass),迫使 GPU 切换当前的帧缓冲(FrameBuffer Binding),这会打断 PixiJS 本身强大的精灵批处理(Sprite Batching)。如果场景中有几百个小精灵,千万不要给每个小精灵单独挂载 Filter;正确的做法是将它们统一放入一个 PIXI.Container 中,仅在该父容器上挂载一次 Filter,一次性完成全局计算。

五、总结与知识体系全景

通过本文的学习,我们完成了从理解 PixiJS Filter 的运行流水线、编写 GLSL 片元算法、到参数动态驱动与避坑优化的全过程。PixiJS 自定义 Filter 的精髓在于利用 GPU 庞大的并行计算能力,在极短时间内完成全画面的像素重映射与色彩合成。

PixiJS 自定义着色器入门:使用 Filters 实现特效
图 4:PixiJS 自定义着色器开发要点与进阶知识全景图

当你熟练掌握了基础的片元着色器后,还可以沿着本文图 4 梳理的知识树继续拓宽能力边界:引入第二张黑白噪声图(Noise Map)制作真实的燃烧溶解特效、编写多 Pass 级联滤镜实现发光光晕(Bloom)、或者结合 PIXI.Mesh 深入到自定义顶点着色器中制作旗帜飘扬与 2.5D 页面翻折效果。着色器为 2D 网页图形开发打开了一扇充满创造力的大门,而 Filter 正是通往这个精彩世界最稳健的第一块跳板。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章