ShaderPass 基础:从原理到第一个自定义滤镜
大家好,我是你们这门课的老朋友。今天咱们来聊聊 Three.js 后处理里最核心的一个概念——ShaderPass。
说实话,我刚接触 Three.js 后处理那会儿,也被 EffectComposer、RenderPass、ShaderPass 这一堆概念搞得有点晕。但后来我发现,只要你搞懂了 ShaderPass,后处理的大门就算被你踹开了。
这一章,我会带你从原理到实战,手写第一个自定义 ShaderPass。嗯,咱们不搞虚的,直接上代码。
ShaderPass 到底是什么?
说白了,ShaderPass 就是一个“滤镜容器”。它接收上一帧渲染好的画面,然后通过你写的着色器(Shader)对每个像素做处理,最后输出新的画面。
你想想看,我们平时用 Photoshop 加滤镜,是不是先有一张原图,然后点一下“模糊”或者“锐化”,图片就变了?ShaderPass 干的就是同样的事——只不过它是在 GPU 上实时处理,每秒 60 帧。
我个人习惯把 ShaderPass 理解成一个“像素加工厂”:
- 输入:上一帧渲染好的纹理(比如场景截图)
- 加工:你写的着色器代码(决定怎么处理每个像素)
- 输出:处理后的新纹理
核心要点:ShaderPass 本质上是一个特殊的 Mesh,它渲染一个全屏四边形,把纹理采样结果经过着色器处理后再输出。你不需要手动创建几何体,Three.js 已经帮你封装好了。
ShaderPass 的工作原理
咱们稍微深入一点。ShaderPass 内部其实做了这几件事:
- 创建全屏四边形:一个覆盖整个视口的平面,只有两个三角形。
- 绑定输入纹理:把上一帧的结果作为纹理传入。
- 执行着色器:对每个像素运行你的片元着色器。
- 输出到帧缓冲:结果写入新的纹理,供下一个 Pass 使用。
这里有个关键点——顶点着色器通常是固定的。为什么?因为全屏四边形不需要任何变换,你只需要传递位置和 UV 坐标就行。我刚开始学的时候,总想着在顶点着色器里搞花样,结果发现根本没必要。你真正需要花心思的,是片元着色器。
小技巧:Three.js 内置了一个 CopyShader,它什么都不做,就是把输入纹理原样输出。你可以把它当成 ShaderPass 的“Hello World”。
编写第一个自定义 ShaderPass
好,理论说够了,咱们动手。我会带你写一个最简单的反色滤镜(Invert Colors)。
先看完整代码,我再逐行解释:
// 1. 定义着色器
const InvertShader = {
uniforms: {
tDiffuse: { value: null } // 输入纹理,名称固定
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
vec4 color = texture2D(tDiffuse, vUv);
gl_FragColor = vec4(1.0 - color.rgb, color.a);
}
`
};
// 2. 创建 ShaderPass
const invertPass = new THREE.ShaderPass(InvertShader);
// 3. 添加到 EffectComposer
composer.addPass(invertPass);
代码不长,但每个部分都有讲究。咱们拆开来看。
uniforms 传递:tDiffuse 的秘密
你注意到 tDiffuse 这个变量名了吗?这是 ShaderPass 的约定。Three.js 会自动把上一帧的渲染结果绑定到这个 uniform 上。你不需要手动赋值,框架帮你搞定。
我在项目中遇到过一个问题:有次我自定义了一个 uniform 叫 inputTexture,结果画面一直是黑的。查了半天才发现,ShaderPass 只认 tDiffuse 这个名称。嗯,这个坑我替你们踩过了。
如果你想传递自己的数据,比如时间、鼠标位置,可以这样:
const MyShader = {
uniforms: {
tDiffuse: { value: null },
uTime: { value: 0 },
uIntensity: { value: 1.0 }
},
// ... 着色器代码
};
// 更新 uniform
myPass.uniforms.uTime.value = performance.now() / 1000;
注意:所有自定义 uniform 都需要在创建 Pass 之后手动更新。Three.js 只会自动处理 tDiffuse。
顶点着色器:为什么几乎不用改?
你看上面的顶点着色器,其实就是标准的 Three.js 模板。它做了两件事:
- 把 UV 坐标传递给片元着色器(
vUv = uv) - 计算顶点位置(
gl_Position)
我建议你直接复制粘贴这个模板,除非你有特殊需求(比如做扭曲效果时需要修改顶点位置)。99% 的情况下,你只需要改片元着色器。
片元着色器:真正干活的地方
反色的逻辑很简单:1.0 - color.rgb。每个颜色通道取反,红色变青色,绿色变品红,蓝色变黄色。
你可以在这里做任何像素级的操作:
- 灰度化:
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114)); - 亮度调整:
color.rgb *= uBrightness; - 颜色偏移:
color.r = texture2D(tDiffuse, vUv + vec2(0.01, 0.0)).r;
避坑指南:我曾经在片元着色器里直接修改 gl_FragColor 的 alpha 值,结果发现透明区域出现了奇怪的边缘。后来才意识到,后处理管线里 alpha 通道的处理需要格外小心,尤其是涉及混合模式的时候。
完整示例:灰度滤镜
咱们再写一个灰度滤镜,巩固一下:
const GrayscaleShader = {
uniforms: {
tDiffuse: { value: null }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
vec4 color = texture2D(tDiffuse, vUv);
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
gl_FragColor = vec4(vec3(gray), color.a);
}
`
};
const grayscalePass = new THREE.ShaderPass(GrayscaleShader);
composer.addPass(grayscalePass);
你看,结构完全一样,只是片元着色器的逻辑变了。这就是 ShaderPass 的威力——你只需要关注像素处理逻辑,其他事情框架帮你搞定。
ShaderPass 与 EffectComposer 的配合
最后提一嘴 ShaderPass 怎么接入管线。你需要在初始化时创建 EffectComposer,然后按顺序添加 Pass:
const composer = new THREE.EffectComposer(renderer);
const renderPass = new THREE.RenderPass(scene, camera);
composer.addPass(renderPass);
const myPass = new THREE.ShaderPass(MyShader);
composer.addPass(myPass);
// 在动画循环中
composer.render();
顺序很重要。第一个 Pass 通常是 RenderPass,它负责渲染场景。后面的 ShaderPass 依次处理上一 Pass 的输出。
总结一下:ShaderPass = 固定的顶点着色器 + 自定义的片元着色器 + 自动绑定的 tDiffuse uniform。你只需要写好片元着色器,就能实现各种炫酷的滤镜效果。
下一章,咱们会深入 AfterimagePass 和 GlitchPass 这些内置特效,看看 Three.js 官方是怎么写复杂 ShaderPass 的。到时候你会发现,理解了基础,复杂的东西也不过是堆叠和组合而已。
好,今天就到这儿。动手试试反色和灰度滤镜,有任何问题咱们评论区见。