在图像渲染完成之后,进行进一步的处理的过程,就叫做图像处理(postprocessing)。
其实就是滤镜。
学过CSS
的人都知道属性filter
,这里面就包含了各式各样的滤镜:模糊、亮度、对比度、灰度、反转之类的。
这些滤镜是是直接就能用的,一行简单的:
filter: xxx();
就搞定了。但是,在Shader
中,并没有任何内置的滤镜函数可以给我们使用,想要实现上面的滤镜效果,我们只能亲自动手去写,不过,这样做会给我们极大的灵活性,我们可以不需要按照公式化的API
去做效果。
染色
第一个滤镜,就是染色滤镜
实现非常简单,把输出结果乘上染色的颜色值就可以了
#iChannel0 "https://s2.loli.net/2024/07/10/oshNYlIBJ5eAu7k.gif"
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec3 tex = texture(iChannel0, uv).xyz;
vec3 col = tex;
vec3 tintColor=vec3(.220,.380,.651);
col *= tintColor;
fragColor = vec4(col, 1.);
}
这样就给整体图片笼罩了一层蓝色。
RGB 位移
看到这个标题你想到了什么?是的,就是抖音的 logo:
抖音的 logo 就是中间一个白色的音符,两侧有青和红的投影,这种其实就是色调偏移的效果。
RGB
位移滤镜和这种就很像,他可以将 R、G、B 三个颜色分量进行偏移,产生一种色差的效果。
我们来尝试实现一下色差的效果:
// 复制三份uv
vec2 rUv = uv;
vec2 gUv = uv;
vec2 bUv = uv;
// 设置一个较大的偏移量数值,方便观察
float offset = .1;
// 给R和B通道进行一正一负的偏移
rUv += offset;
bUv -= offset;
// 采样三次纹理
vec4 rTex = texture(iChannel0, rUv);
vec4 gTex = texture(iChannel0, gUv);
vec4 bTex = texture(iChannel0, bUv);
vec4 col = vec4(rTex.r, gTex.g, bTex.b, gTex.a);
fragColor = vec4(col);
效果出现了,偏移量设置的 0.1,主要是看效果。
我们把偏移量变更为 0.009,就能看到可以看出来的色差,但是不会突兀的感觉。
除了一般的固定便宜之外,我们还可以把上一篇的“随机”拿出来用,给偏移整点噪声。
完整的代码:
#iChannel0 "https://s2.loli.net/2024/07/10/oshNYlIBJ5eAu7k.gif"
// 随机函数
highp float random(vec2 co)
{
highp float a=12.9898;
highp float b=78.233;
highp float c=43758.5453;
highp float dt=dot(co.xy,vec2(a,b));
highp float sn=mod(dt,3.14);
return fract(sin(sn)*c);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec3 tex = texture(iChannel0, uv).xyz;
// 为了将噪声值控制在0到1
float noise=random(uv)*.5+.5;
// 使用sin和cos调整一下噪点的偏移量位置
vec2 offset=.0075*vec2(cos(noise),sin(noise));
// 复制三份uv
vec2 rUv = uv;
vec2 gUv = uv;
vec2 bUv = uv;
// 给R和B通道进行一正一负的偏移
rUv += offset;
bUv -= offset;
// 采样三次纹理
vec4 rTex = texture(iChannel0, rUv);
vec4 gTex = texture(iChannel0, gUv);
vec4 bTex = texture(iChannel0, bUv);
vec4 col = vec4(rTex.r, gTex.g, bTex.b, gTex.a);
fragColor = vec4(col);
}
把噪点的值设置为 0.075 是为了查看噪点的具体分布,可以将值自行调整观察。
在这里贴一张噪点的图:
挤压
在 PS 里面,有一个叫做挤压滤镜的东西,大概是这样的:
在 Shader 里面,我们一样可以通过代码来做出一样的效果。
首先,我们创建一下负责膨胀或者挤压的函数bulge
vec2 bulge(vec2 p) {
return p;
}
咱们在主函数mainImage
中应用膨胀函数bulge
。
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
uv = bulge(uv);
vec3 tex = texture(iChannel0, uv).xyz;
fragColor = vec4(tex, 1.);
}
接下来,我们来编写膨胀函数吧: 1.用length
计算uv
上每个点到原点的距离,然后跟uv
直接相乘。
float d = length(p);
p *= d;
长这样了:
这个效果是不太对的,因为UV
的原点不在画布中心,在画布左下角,我们手动选择一个画布正中心的值吧:
vec2 center = vec2(.5);
在调用length
函数之前,我们先把UV
减去这个中点,然后调用之后加回去。
p -= center;
float d = length(p);
p *= d;
p += center;
完美复刻了之前 PS 的效果:
当然,我们还可以对效果做一下加强,我们把d
的距离应用一下pow
函数做一个指数增加,效果会更加明显。
float dPow = pow(d, 2.);
// p *= d;
p *= dPow;
效果有点鬼畜了,就不贴图了。
截止到现在,我们做的挤压实际上都是外凸的,有什么办法能做到内凹吗?
其实很简单,我们只需要给uv
乘上距离的倒数就行了。
float dRev = 1./dPow;
p *= dRev;
让我们来试一下:
呃啊,辣眼睛的万花筒出现了,为什么会这样?
因为dPow
很可能是一个非常小的值,比如说(0.00001)乃至无穷小,那么它的倒数就会变成非常大,乃至无穷大。就会出现上述的万花筒。
我们要解决它,其实也非常的简单,我们给底数加一个 1,让这个整体不至于非常的小就行:
float dRev = 1. / (dPow + 1.);
最后,我们来微调一下这个内凹效果,并且加上与用户鼠标进行互动的逻辑,要怎么做?增加两个额外的参数,扭曲的半径radius
以及扭曲的强度strength
。
#iChannel0 "https://s2.loli.net/2024/07/10/oshNYlIBJ5eAu7k.gif"
// 挤压函数
vec2 bulge(vec2 p) {
vec2 center = iMouse.xy / iResolution.xy;
float radius = .9;
float strength = 1.1;
p -= center;
float d = length(p);
d /= radius;
float dPow = pow(d, 2.);
float dRev = strength / (dPow + 1.);
p *= dRev;
p += center;
return p;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
uv = bulge(uv);
vec3 tex = texture(iChannel0, uv).xyz;
fragColor = vec4(tex, 1.);
}
来讲解一下代码,其中:
radius
表示扭曲效果的影响半径,radius
的值越大,更多的区域会受到扭曲效果的影响。strength
表示扭曲效果的强度,strength
的值越大,扭曲效果越强,纹理坐标的变化越大。d /= radius
,将距离d
归一化,使其相对于radius
。如果d
小于radius
,则d
的值在[0, 1]
范围内;如果d
大于radius
,则d
的值大于 1。
最后,来看看效果吧:
还挺有意思的!
像素化
也就是我们经常看到的,马赛克。将特定区域变成方块,遮挡敏感信息。
在 Shader 里,马赛克就是像素,就等于是把画面像素化。
我们现在就会接触一个新的函数floor
,函数的作用就是向下取整,比如说floor(114.514)
就是114.
我们来举一个例子,我们对uv
的x
轴进行输出,将x
的坐标乘上像素化大小的值,并且使用 floor 函数来向下取整,然后再除以像素化大小的值。
void mainImage(out vec4 fragColor,in vec2 fragCoord) {
vec2 uv = fragCoord/iResolution.xy;
float c = uv.x;
c = floor(c * 10.) / 10.;
fragColor = vec4(vec3(c), 1.0);
}
好,他现在长这样的:
为什么?
来我们去 Graphtoy 看看这个函数的效果:
简单来说,举一个值的例子吧:
比如我的x
坐标位于(0, 0.09)
的时候,x*10
的值域是(0, 0.9)
,但是floor
函数是向下取整的,所以floor(x*10)
的值是0
,除以10
还是0
;但当x
坐标位于0.1
的时候,x*10
就变为了1
,这个时候的向下取整就成了1
,除以10
就变为了0.1
;之后的范围值就会以0.1
的步长递增,公式总体是阶梯的形状了。
那我们回到一开始的采样纹理吧:
#iChannel0 "https://s2.loli.net/2024/07/10/oshNYlIBJ5eAu7k.gif"
void mainImage(out vec4 fragColor,in vec2 fragCoord) {
vec2 uv = fragCoord/iResolution.xy;
vec2 size = vec2(50., 50.);
uv=floor(uv*size)/size;
vec3 tex=texture(iChannel0,uv).xyz;
fragColor=vec4(tex,1.);
}
心中无码,自然高清。马赛克,打好了!
晕影
在去澳门之前拿到了一副佳能相机,随之研究了一下摄影手法,其中,接触到一种特殊的手法叫做晕影
在摄影和光学领域内,晕影或暗角是指图像的外围部分的亮度或饱和度比中心区域低。
有时却因为需要创意风格而刻意加入,例如引起对图像中心区的注意。摄影师也会故意选择能产生晕影的镜头来制造特效,也可以使用特别的滤镜或以后期处理来制造晕影。
比如:
我们来用Shader
来尝试实现一下吧。
为了观察这个效果的实现,我们先把画布设置为白色。
void mainImage (out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec3 col = vec3(1.);
fragColor = vec4(col, 1.);
}
紧接着,我们在vec3 col = vec3(1.)
下一行进行编码,我们来创建一个圆形的径向转变,这一步骤就是画个圆的步骤,详情可以参考本教程的 uv 相关的章节
因为我们要把效果拉伸到整个画布,所以不需要对比例进行修改
vec2 p = uv;
p -= .5;
float d = length(p);
col = vec3(d);
这是一个简单的径向渐变,接下来,注释掉col = vec3(d);
我们来使用smoothstep
来把这个渐变效果拉伸一下,并且把边界值设置的稍微差距大一点,可以塑造一种模糊的感觉:
col = vec3(d);
float c = smoothstep(.4, .8, d);
col = vec3(c);
接下来是关键的一步,把这个渐变效果翻转过来,我们就可以得到晕影的效果了,要怎么做?
有两种做法,效果是一样的,根据个人喜好即可 1.使用 1 减去c
的值 2.调换smoothstep
的边界值
出于个人喜好,我选择了第一种,第二种也是完全没有问题的,原理也是非常的简单,前面的文章也充分介绍过了。
之前演示的 GBC 的人物图背景黑色有点不是很明显,我们换回上一章节使用的那张图,并且加上晕影滤镜试一下:
全部代码:
#iChannel0 "https://s2.loli.net/2023/09/10/QozT59R6KsYmb3q.jpg"
void mainImage (out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec3 tex = texture(iChannel0, uv).xyz;
vec3 col = tex;
vec2 p = uv;
p -= .5;
float d = length(p);
col = vec3(d);
float c = smoothstep(.4, .8, d);
col = vec3(1. - c) * tex;
fragColor = vec4(col, 1.);
}
嗯,效果很好,这就完成了。
这里介绍的滤镜知识很简单,都是一些基础的滤镜,各位可以根据诸如 PS 的工具上的滤镜思考一下 Shader 如何去实现一样的效果,其实都是操作像素和画布,本质是一样的东西!