前些日子偶然看见群友分享的车展场景的视频,车辆的尾灯亮起的时候,屏幕有水平脏镜的效果,想起之前在Keijiro大佬的Git上面看到过这个效果(https://github.com/keijiro/Kino),原理也十分简单,遂拿来和大家一起学习分享。
https://github.com/keijiro/Kino
发文前测试用PG:https://playground.babylonjs.com/#0S31R1#7
这个场景我使用一个简单的Shader实现了一个按uv.y分段随机高亮的效果,这个Shader使用round函数沿Y轴对模型进行分层,然后使用非常常用的noise函数随机颜色和亮度。由于我们还没有使用后处理开启HDR纹理模式,颜色会显得比较苍白(https://playground.babylonjs.com/#APCGJ1#1)
float rand(vec2 n) {
return fract(cos(dot(n, vec2(12.9898, 4.1414))) * 43758.5453);
}
float noise(vec2 n) {
const vec2 d = vec2(0.0, 1.0);
vec2 b = floor(n);
vec2 f = smoothstep(vec2(0.0), vec2(1.0), fract(n));
return mix(mix(rand(b), rand(b + d.yx), f.x), mix(rand(b + d.xy), rand(b + d.yy), f.x), f.y);
}
void main(void)
{
float c = vUV.x;
float y = round(vUV.y * 30.)/30.;
float scale = 100.;
float cy = noise(vec2(vUV.y*scale,vUV.y*scale));
float cr = noise(vec2(vUV.y*scale+scale,vUV.y*scale+scale));
float cg = noise(vec2(vUV.y*scale+scale*2.,vUV.y*scale+scale*2.));
float cb = noise(vec2(vUV.y*scale+scale*3.,vUV.y*scale+scale*3.));
vec3 randC = vec3(cr,cg,cb);
gl_FragColor = vec4(vec3(pow(c * cy,8.)*100.) * randC, 1.);
}
//
想要制作Streak后处理,我们有两件事需要做:提取水平脏镜的发光主体和水平拉丝。
1.第一件事我们有两种方式实现,第一种规则是高亮发光,类似于Babylon DefaultRenderPipeline的Bloom效果,我们在HDR模式渲染场景,使用一个Threshold去截取高亮部分,让高亮部分作为发光的源头,第二种方法是使用Mask把希望发光的部分标记,然后让标记的部分作为发光的源头。这两种方法有不同的应用场景,如果希望水平脏镜是全局的效果,可以使用第一种方法,如果只希望部分物体脏镜且不希望它们过亮影响场景的观感,选择第二种(如汽车的尾灯),本文为了方便使用第一种方法提取发光部分。我们使用第一个EffectWrapper来提取高亮部分,由于水平脏镜是一种非常夸张的模糊效果,对纹理的精度要求不是非常的高,为了降低计算量,我们配置的RTT(RenderTargetTexture)的高度只有原窗口的一半,同时采样的时候做了一部分额外的竖直方向的平滑,我们拿从平滑得到的颜色取RGB三值中的最大值和Threshold做比较,得到选取的高亮部分。
Streak_Down_0.png
precision highp float;
varying vec2 vUV;
uniform sampler2D textureSampler;
uniform vec2 u_Resolution;
//x:threshold y:stretch, z:intensity
uniform vec4 streakParams;
void main(void)
{
//竖直方向的平滑
vec2 ss = vUV - vec2(0.,0.5/u_Resolution.y);
vec2 ss1 = vUV + vec2(0.,0.5/u_Resolution.y);
vec3 c0 = texture2D(textureSampler,ss).rgb;
vec3 c1 = texture2D(textureSampler,ss1).rgb;
vec3 c = (c0 + c1) / 2.;
//选取RGB中最大值去和threshold作比较
float br = max(c.r, max(c.g, c.b));
float _Threshold = streakParams.x;
c *= max(0., br - _Threshold) ;
gl_FragColor = vec4(c, 1.);
// gl_FragColor = vec4(vec3(max(0., br-1. )),1.);
}
第二件事是水平拉丝,这个过程比较繁琐。首先我们可以看一个Demo,这个demo主要是将图像强制缩小到宽度较小的纹理中再强制拉伸到原来的尺寸中,按↑↓键可以调整缩小的比例(https://playground.babylonjs.com/#UHAXWA#1)。当按↑到一定程度就出现了水平拉丝的图像,但是这种方法很明显存在两个问题:拉丝宽度不够以及没办法细节控制。
简单拉丝图
为了解决问题并实现更好的效果,我们可以对上述过程进行优化,首先我们舍弃一次性压缩,选择以2为单位分多次对原图进行水平压缩,在每一次压缩时,对上一级的纹理进行一步水平方向的模糊,模糊的效果可以沿着缩小的次数指数传递到最后一张图,使得后面的低级图有更强的模糊效果。
precision highp float;
varying vec2 vUV;
uniform sampler2D textureSampler;
uniform vec2 u_Resolution;
void main(void)
{
vec2 uv = vUV;
float dx = 1./u_Resolution.x;
float u0 = uv.x - dx * 5.;
float u1 = uv.x - dx * 3.;
float u2 = uv.x - dx * 1.;
float u3 = uv.x + dx * 1.;
float u4 = uv.x + dx * 3.;
float u5 = uv.x + dx * 5.;
vec3 c0 = texture2D(textureSampler, vec2(u0, uv.y)).rgb;
vec3 c1 = texture2D(textureSampler, vec2(u1, uv.y)).rgb;
vec3 c2 = texture2D(textureSampler, vec2(u2, uv.y)).rgb;
vec3 c3 = texture2D(textureSampler, vec2(u3, uv.y)).rgb;
vec3 c4 = texture2D(textureSampler, vec2(u4, uv.y)).rgb;
vec3 c5 = texture2D(textureSampler, vec2(u5, uv.y)).rgb;
vec3 c = (c0 + c1 * 2. + c2 * 3. + c3 * 3. + c4 * 2. + c5) / 6.;
gl_FragColor = vec4(c, 1.);
}
在一步的计算下,我们可以得到从短到长的一系列拉丝图(如拼接图1),明显这些图已有水平拉丝的雏形,一般我们希望靠近发光点的地方亮,然后朝着发光点的两侧渐弱,我们可以从右向左依次混合,亮度的权重从右向左依次增加,就可以产生中间亮两边弱的图像(如拼接图2)。
拼接图1
拼接图2
这一步我们使用了一个新的Pass对纹理进行混合(如下文),我们在这个Pass里面添加了两个控制参数:_Stretch 和_StretchIntensity ,其中_Stretch是右边低级图叠加的权重,数值越小衰减越快,_StretchIntensity 是右边低级图的亮度。
precision highp float;
varying vec2 vUV;
uniform sampler2D textureSampler;
uniform sampler2D _HighTexture;
uniform vec2 u_Resolution;
//x:threshold y:stretch, z:intensity
uniform vec4 streakParams;
void main(void)
{
vec2 uv = vUV;
float dx = 1./u_Resolution.x *1.5;
float u0 = uv.x;
float u11 = uv.x - dx;
float u12 = uv.x + dx;
float u21 = uv.x - dx * 2.;
float u22 = uv.x + dx * 2.;
vec3 c0 = texture2D(textureSampler, vec2(u0, uv.y)).rgb;
vec3 c11 = texture2D(textureSampler, vec2(u11, uv.y)).rgb;
vec3 c12 = texture2D(textureSampler, vec2(u12, uv.y)).rgb;
vec3 c21 = texture2D(textureSampler, vec2(u21, uv.y)).rgb;
vec3 c22 = texture2D(textureSampler, vec2(u22, uv.y)).rgb;
vec3 c3 = texture2D(_HighTexture, uv).rgb;
float _Stretch = streakParams.y;
float _StretchIntensity = streakParams.z;
gl_FragColor = vec4(mix(c3, (c0+ (c11+c12)/2.+(c21+c22)/4.)*_StretchIntensity, _Stretch), 1.);
}
上面几步完成了拉丝的效果,接下来要将获取的拉丝图和原图进行叠加,这一步我们对拉斯图进行进一步的水平模糊处理,添加了一个参数_Intensity控制水平拉丝光的强度。
precision highp float;
varying vec2 vUV;
uniform sampler2D textureSampler;
uniform sampler2D streakTex;
uniform vec2 u_Resolution;
//x:threshold y:stretch, z:intensity
uniform vec4 streakParams;
uniform vec3 _Color;
void main(void)
{
vec2 uv = vUV;
float dx = 1./u_Resolution.x * 1.5;
float u0 = uv.x - dx;
float u1 = uv.x;
float u2 = uv.x + dx;
vec3 c0 = texture2D(streakTex,vec2(u0, uv.y)).rgb;
vec3 c1 = texture2D(streakTex,vec2(u1, uv.y)).rgb;
vec3 c2 = texture2D(streakTex,vec2(u2, uv.y)).rgb;
vec3 c3 = texture2D(textureSampler,vUV).rgb;
float _Intensity = streakParams.w;
vec3 cf = (c0 / 4. + c1 / 2. + c2 / 4.) * _Color * _Intensity * 5.;
gl_FragColor = vec4(cf + c3, 1.);
}
最终大功告成,完成了水平拉丝的后处理效果,可以访问PG:https://playground.babylonjs.com/#0S31R1#8体验后处理的效果。如图,选中pipeline节点,通过自定义字段修改后处理的四个参数。