后处理(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…