深入WebGL后处理特效——掌握变形技

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

概述

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

美颜相机的变形特效

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

  • 局部扭曲 (twirl effect)
  • 局部膨胀 (inflate effect)
  • 任意方向挤压 (pinch effect)

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

变形技原理

虽然变形的效果千奇百怪,但它们往往离不开这三个要素:变形位置、影响范围和变形程度。

变形Shader实现人物尬舞

因此它在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方法在扭曲边界处呈现更为平滑,如下图。

lnear和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

如上图,膨胀函数入参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的实现原理,其中膨胀/收缩和挤压效果是通过采样距离场变换实现的,前者变换的是采样圈大小,后者变换的是采用圈位置。
除了上文的介绍的三种局部变形,还有一些比较有趣的全局变形效果,比如波浪特效(wave effect),错位特效和镜像等,shader实现比较容易,就不多做介绍了。

波浪-错位-镜像

预览代码与效果

扭曲:https://www.shadertoy.com/view/slfGzN
膨胀/缩放:https://www.shadertoy.com/view/7lXGzN
挤压/拉伸:https://www.shadertoy.com/view/7tX3zN

参考资料:

glsl基础变换:https://thebookofshaders.com/08/?lan=ch
Photoshop挤压特效算法:https://blog.csdn.net/kezunhai/article/details/41873775

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容