后处理(Post-processing),是针对原有的游戏画面进行算法加工,达到晋升画面质量或加强画面成果的技术,可通过着色器Shader程序实现。

概述

变形特效是解决和加强画面成果的一类后处理技术,常常被利用在各类相机短视频app特效中,如美颜瘦身、哈哈镜特效。

本文次要从各类美颜相机中梳理了以下几种罕用的变形特效:

  • 部分扭曲 (twirl effect)
  • 部分收缩 (inflate effect)
  • 任意方向挤压 (pinch effect)

其中,扭曲可用在眼睛的部分旋转,收缩能够用于大眼,挤压/拉伸可用于脸部塑性和瘦脸等。如何通过着色器Shader实现这些变形,是本文探讨的重点。(ps:焦急预览代码的童鞋见文末)

变形技原理

尽管变形的成果千奇百怪,但它们往往离不开这三个因素:变形地位、影响范畴和变形水平。

因而它在Shader中的实现,就是通过结构一个变形函数,将传入原始uv坐标,变形的地位、范畴range和水平strength,通过计算后生成变形后的采样坐标,代码如下:

#iChannel0 "src/assets/texture/joker.png"vec2 deform(vec2 uv, vec2 center, float range, float strength) {  // TODO: 变形解决  return uv;}void mainImage(out vec4 fragColor, vec2 coord) {    vec2 uv = coord / iResolution.xy;    vec2 mouse = iMouse.xy / iResolution.xy;    uv = deform(uv, mouse, .5, .5);    vec3 color = texture(iChannel0, uv).rgb;    fragColor.rgb = color;}
本文着色器代码采纳GLSL标准,遵循Shader-Toy的写法,不便大家预览。

变形小技巧:采样间隔场变换

咱们设置定点坐标O,任意点到点O间隔为dist,以不同dist值为半径,以点O为核心可造成无数个等距的采样圈,它们被称为点O的间隔场。

咱们能够通过扭转采样圈的大小、地位,进而扭转纹理采样地位,以实现收缩/膨胀、挤压/拉伸的变形成果。

vec2 deform(vec2 uv, vec2 center, float range, float strength) {  float dist = distance(uv, center);  vec2 direction = normalize(uv - center);  dist = transform(dist, range, strength); // 扭转采样圈半径  center = transform(center, dist, range, strength); // 扭转采样圈核心地位  return center + dist * direction;}

这个技巧的利用先不急着说,当初咱们还是从简略的扭曲变形开始讲。

扭曲

扭曲成果相似旋涡状态,特点是越凑近中心点旋转水平越激烈,咱们可通过递加函数来示意离中心点间隔d和对应旋转角度之间的关系。

如下图,采纳简略的一次函数 = -A/R *d + A,其中A示意扭曲核心的旋转角度,A为负数则示意旋转方向为顺时针,正数示意逆时针,R示意扭曲的边界;

如上图,扭曲函数入参A(核心旋转角Angle)和R(变形范畴Range)能够这么形容:
1)A代表核心旋转角度,绝对值越大,扭曲水平更高;
2)A > 0示意扭曲方向为顺时针,反之A<0示意逆时针;
3)R代表扭曲边界,值越大,影响范畴越大。

咱们能够引入工夫变量time动静扭转A的值,产生扭动特效,如上图小丑扭跨成果,具体shader代码如下:

#iChannel0 "src/assets/texture/joker.png"#define Range .3#define Angle .5#define SPEED 3.mat2 rotate(float a) // 旋转矩阵{    float s = sin(a);    float c = cos(a);    return mat2(c,-s,s,c);}vec2 twirl(vec2 uv, vec2 center, float range, float angle) {    float d = distance(uv, center);    uv -=center;    // d = clamp(-angle/range * d + angle,0.,angle); // 线性方程    d = smoothstep(0., range, range-d) * angle;    uv *= rotate(d);    uv+=center;    return uv;}void mainImage(out vec4 fragColor, vec2 coord) {    vec2 uv = coord / iResolution.xy;    vec2 mouse = iMouse.xy / iResolution.xy;    float cTime = sin(iTime * SPEED);    uv = twirl(uv, mouse, Range, Angle * cTime);    vec4 color = texture(iChannel0, uv);    fragColor = color;} 

值得一提的是,除了用线性方程示意扭曲关系,还能够应用smoothstep办法,相比linear线性函数,smoothstep办法在扭曲边界处出现更为平滑,如下图。

思考到边界的平滑,上面的变形办法也多会用smoothstep函数来代替线性方程。

收缩/膨胀

收缩特点凑近收缩核心的纹理被拉伸,而凑近收缩边界纹理被挤压,这意味着在收缩范畴内,以收缩核心为间隔场,每个采样圈都应该比原先的半径更小,并且圈间距由内到外逐步扩充。

如下图右侧,咱们通过将等距的彩色采样圈映射到更内聚的红色采样圈,使新采样圈之间的间距由内到外枯燥递增。

咱们采样平滑递增函数smoothstep来通过采样圈半径dist计算出缩放值scale:

上图的函数表明,在凑近收缩核心处,采样圈缩放最显著,缩放值最小(1 - S);随着dist增大,缩放值scale往1递增,直至达到R边界范畴后,scale恒定为1,采样圈不再缩放。

 float scale = (1.- S) + S * smoothstep(0.,1., dist / R); // 计算收缩采样半径缩放值

于是咱们失去上述采样半径缩放公式,其中设定Strength(0 < S < 1)代表收缩水平。
对于收缩间隔场的变换过程,很容易推断出,要实现收缩的反向成果膨胀,间接让S位于[-1,0]区间即可。

如上图,收缩函数入参S(变形水平Strength)和R(变形范畴Range)可这么形容:
1)当S在[0,1]区间时,出现收缩成果,S值越大,收缩的水平越高;
2)当S在[-10]区间时,出现膨胀成果,S值越小,膨胀水平越高;
3)R代表变形的边界,值越大时,影响区域越大;

咱们能够引入工夫变量time动静扭转Strength的值,模仿呼吸动画,如上图小丑鼓肚子成果,具体shader代码如下:

#iChannel0 "src/assets/texture/joker.png"#define SPEED 2. // 速度#define RANGE .2 // 变形范畴#define Strength .5 * sin(iTime * SPEED) // 变形水平vec2 inflate(vec2 uv, vec2 center, float range, float strength) {    float dist = distance(uv , center);    vec2 dir = normalize(uv - center);    float scale = 1.-strength + strength * smoothstep(0., 1. ,dist / range);    float newDist = dist * scale;    return center + newDist * dir;}void mainImage(out vec4 fragColor, vec2 coord) {    vec2 uv = coord / iResolution.xy;    vec2 mouse = iMouse.xy / iResolution.xy;    uv = inflate(uv, mouse, RANGE, Strength);    vec3 color = texture(iChannel0, uv).rgb;    fragColor.rgb = color;}

纵向/横向拉伸

后面的收缩是通过对间隔场采样圈进行缩放实现的,纵向/横向拉伸则是只对采样圈x轴或y轴进行缩放,个别可用在美颜的“长腿特效”上。

能够发现横向拉伸间隔场被变换为多个椭圆采样圈,代码实现如下:

vec2 inflateX(vec2 uv, vec2 center, float radius, float strength) {    // 后面代码跟收缩实现一样    ...    return center + vec2(newDist, dist) * dir; // 横向拉伸则scale只作用于想x轴}

挤压

挤压个别会指明一个作用点和一个挤压方向,它的特点是把作用点左近的纹理推到挤压起点地位。

如下图,绿色作用点P作为挤压终点,箭头为挤压向量V,其中向量方向指明挤压的方向,向量长度 length(V)代表挤压的间隔,向量起点为挤压后的地位。
要实现纹理挤压,就是让采样圈圆心往挤压向量V上偏移,采样中心点应平移到点P的地位。

随着采样圈的半径dist由内到外逐步变大,其变换后的圆心偏移量offset逐步缩短,咱们能够用-smoothstep平滑递加函数解决采样圈半径dist与圈偏移量offset之间的关系。

公式:offset = length(V) - length(V) * smoothstep(0, R, dist),其中R示意挤压边界range。

同样的,咱们引入工夫变量time动静扭转挤压向量的长度和方向,能够实现抖动特效,如上图小丑顶胯成果,具体shader代码如下:

#iChannel0 "src/assets/texture/joker.png"#define RANGE .25  // 变形范畴#define PINCH_VECTOR vec2( sin(iTime * 10.), cos(iTime * 20.)) * .03 // 挤压向量vec2 pinch(vec2 uv, vec2 targetPoint, vec2 vector, float range) {     vec2 center = targetPoint + vector;    float dist = distance(uv, targetPoint);    vec2 point = targetPoint +  smoothstep(0., 1., dist / range) * vector;    return uv - center + point;}void mainImage(out vec4 fragColor, vec2 coord) {    vec2 uv = coord / iResolution.xy;    vec2 mouse = iMouse.xy / iResolution.xy;    uv = pinch(uv, mouse, PINCH_VECTOR, RANGE);    vec3 color = texture(iChannel0, uv).rgb;    fragColor.rgb = color;}

总结

本文次要介绍三类部分变形shader的实现原理,其中收缩/膨胀和挤压成果是通过采样间隔场变换实现的,前者变换的是采样圈大小,后者变换的是采纳圈地位。

除了这三种部分变形,还有一些乏味的全局变形比方波浪、错位和镜像等特效,shader实现比拟容易,就不多做介绍了。

预览代码与成果

扭曲:https://www.shadertoy.com/vie...
收缩/缩放:https://www.shadertoy.com/vie...
挤压/拉伸:https://www.shadertoy.com/vie...

相干文章

走进图形噪声:https://zhuanlan.zhihu.com/p/...
glsl根底变换:https://thebookofshaders.com/...
Photoshop挤压特效算法:https://blog.csdn.net/kezunha...