作为一个前端开发的人员,基本上离不开一个用来显示图片的标签,那就是img

业界有云:UI给出来的效果我实现不出来?那就让UI负责切图吧!

在Shader里,没有img,取而代之的是纹理

接下来这节内容就是把纹理,带入我们的程序中了。

启动模板

这边提供一个简单的启动模板和测试用图片链接:

#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;
    fragColor=vec4(tex,1.);
}   

注意,关于上述的代码的一些补充说明,在之前的文章里,我们提到了Shader的编写方式分为:

  • GLSL文件内,并且使用了vscode以及插件Shader Toy来预览
  • Shadertoy网站上引入编写的

那么,如果你是在GLSL文件内引入的,就可以直接参考我的代码来引入纹理

如果,你是在Shadertoy网站上编写的Shader,步骤如下:

  1. 点击右下角的黑框iChannel0
  2. 点击“纹理”,然后从下面的图片列表中随便选一张即可,选完后点击右上方的叉叉关闭。

接下来,引入了纹理,我们为了将它显示在屏幕上,要进行的就是采样操作,GLSL内置了一个采样函数texture,我们直接用这个将纹理给采样出来就行了,就是我上面发出的代码了。

采样函数texture接受两个参数:第一个是我们的纹理iChannel0,第二个是UV坐标变量uv,采样的结果命名是tex,取得它的前3维度(暂时不需要考虑纹理的透明度),最后用变量vec4(tex, 1.)输出到屏幕上。

顺利的话,我们看到的是这样的:

这里要特别警告一下:

对于用编辑器的朋友们来说,有可能有人在运用HTML搭载GLSL的时候,屏幕会变成黑色,并且控制台会报错,红色的错误。原因就在于,你的浏览器可能不支持WebGL2,有两个办法:1.升级你的浏览器;2.将纹理采样函数texture改名为texture2D。

好,我们将纹理搬到了屏幕上,接下来就是要对他进行一些有意思的操作了!

扭曲

第一种操作是扭曲,通过改变纹理的UV坐标来扭曲整个图片的形状。

让我们来创建一个扭曲函数,就叫做distort吧。同时将uv经过他的处理,再交由texture采样函数:

#iChannel0 "https://s2.loli.net/2023/09/10/QozT59R6KsYmb3q.jpg"

vec2 distort (vec2 uv) {
    uv.x += sin(uv.y);
    return uv;
}

void mainImage(out vec4 fragColor,in vec2 fragCoord){
    vec2 uv=fragCoord/iResolution.xy;
    vec3 tex=texture(iChannel0,distort(uv)).xyz;
    fragColor=vec4(tex,1.);
}   

效果就是这样的:

我们可以看到这图片被夸张地扭曲了,扭曲程度太大,我们需要对扭曲量进行调整

调试函数

这里就要用到一个非常不错的网站Graphtoy,是的,这也是iq几何大佬创建的网站,它可以非常方便的对Shader的函数进行可视化的调试。

首次进入网站的时候,它会展示好几张函数的默认图像,我们直接跳过,点击Clear按钮清空,我们输入sin(x):

sin函数默认的变化幅度是蛮大的,我们要缩小幅度,要怎么做呢,那就是给x乘上一个数字,相当于变相的增加了sin函数的频率

我们再整体除以50,其实相当于减少了sin函数的振幅

如果我们再给sin函数内部的值加上t,也就是时间参数,他就会动起来:

所以我们可以做一个有规律的律动效果:

#iChannel0 "https://s2.loli.net/2023/09/10/QozT59R6KsYmb3q.jpg"

vec2 distort (vec2 uv) {
    uv.x += sin(uv.y * 10. + iTime) / 50.;
    return uv;
}

void mainImage(out vec4 fragColor,in vec2 fragCoord){
    vec2 uv=fragCoord/iResolution.xy;
    vec3 tex=texture(iChannel0,distort(uv)).xyz;
    fragColor=vec4(tex,1.);
}   

转场

第二种操作是转场,简而言之就是从一张纹理转变成另外一张纹理。

这种操作需要两张图片。
引入第二张:

#iChannel1 "https://s2.loli.net/2023/09/10/Jb8mIhZMBElPiuC.jpg"

我们可以再声明一张。引入第二张纹理之后,创建两个采样函数,用于将刚刚声明的两个纹理采样出来。然后我们就会用到转场函数transition。转场函数其实就是通过mix函数将两张纹理混合了起来,混合程度是用户鼠标归一化之后的x轴位置。
下面是示例代码:

vec4 getFromColor(vec2 uv) {
    return texture(iChannel0, uv);
}

vec4 getToColor(vec2 uv) {
    return texture(iChannel1, uv);
}

vec4 transition(vec2 uv) {
    float progress = iMouse.x / iResolution.x;
    return mix(getFromColor(uv), getToColor(uv), progress);
}

void mainImage (out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    vec4 col = transition(uv);
    fragColor = col;
}

左右拖拽鼠标,就可以看到很简单的转场效果,也就是淡入淡出转场效果。

滑动转场

淡入淡出倒是很简单,那么,我们回忆一下之前学习uv的时候,学过的step函数。它接受 2 个参数:边界值edge和目标值x,如果目标值x大于边界值edge,则返回 1,反之返回 0。

那我们给代码加上这个函数,会发生什么?

vec4 getFromColor(vec2 uv) {
    return texture(iChannel0, uv);
}

vec4 getToColor(vec2 uv) {
    return texture(iChannel1, uv);
}

vec4 transition(vec2 uv) {
    float progress = iMouse.x / iResolution.x;
    return mix(getFromColor(uv), getToColor(uv), 1. - step(progress, uv.x));
}

void mainImage (out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    vec4 col = transition(uv);
    fragColor = col;
}

效果图:

我们得到了一个像帘子一般的左右滑动的转场效果。
下面我标记一下这个效果的原理图:

将画布分成两部分,位于鼠标左边的部分(左半),以及,位于鼠标右边的部分(右半),左半部分的uv.x是小于progress的,step函数会返回0,mix混合程度则返回1 - 0也就是1,表示的是第二张图片;右半部分的uv.x是大于progress的,step函数会返回1,那么,混合程度就是1 - 1也就是0,表示的是第一张图片。

遮罩转场

那么我们接下来,我们再来换一种形式。在progress变量的下方新增一个ratio变量,表示画布的比例。

我们这个例子来一个圆形转场的遮罩,用我们的SDF函数以及上面提到的progress来设定圆形遮罩的半径,不够大的话,乘上一个数字(比方说根号2),然后对遮罩取反就设置为我们的混合程度。另外我们将smoothstep的范围再稍微调整一下,这样就可以实现比较完美的效果。

全部代码如下:

float sdCircle(vec2 p,float r)
{
    return length(p)-r;
}


vec4 getFromColor(vec2 uv) {
    return texture(iChannel0, uv);
}

vec4 getToColor(vec2 uv) {
    return texture(iChannel1, uv);
}

vec4 transition(vec2 uv) {
    float progress = iMouse.x / iResolution.x;
    float ratio=iResolution.x/iResolution.y;
    vec2 p = uv;
    p -= .5;
    p.x *= ratio;
    float d = sdCircle(p, progress*sqrt(2.));
    float c = smoothstep(-.2, 0., d);
    return mix(getFromColor(uv), getToColor(uv), 1. - c);
}

void mainImage (out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;
    vec4 col = transition(uv);
    fragColor = col;
}

效果图:

这样,我们就得到了一个虚化的圆形遮罩转场效果。

置换转场

我们还可以用纹理本身来扭曲纹理的uv坐标,这样的操作称之为“置换”,用来扭曲的纹理称之为“置换纹理”。

我们引入第三张纹理:

#iChannel2 "https://s2.loli.net/2023/07/17/3GDlwcvehqQjTPH.jpg"

来编写转场函数transition

vec4 transition(vec2 uv){
    float progress=iMouse.x/iResolution.x;
    float ratio=iResolution.x/iResolution.y;

    vec2 dispVec=texture(iChannel2,uv).xy;
    vec2 uv1=vec2(uv.x-dispVec.x*progress,uv.y);
    vec2 uv2=vec2(uv.x+dispVec.x*(1.-progress),uv.y);
    return mix(getFromColor(uv1),getToColor(uv2),progress);
}

这里我们要做的就是用置换纹理去扭曲两张图片的uv

将置换纹理采样成dispVec,取出前两个维度。

根据我们在UV相关内容的学习中,往正方向(右)移动的话,不是加上,而是减去,所以我们给uv的x坐标减了x轴的偏移量dispVec.x,乘上progress之后,让他变得能够随着鼠标的位置变化而变化,第二张图片也是同理,方向取反,鼠标位置也要取反。

为什么uv.x - dispVec.x就会让图片呈现纹理效果?

答案:因为置换纹理本身的所有像素就存储了位移的距离,这是3D业界一种固有的做法,叫“凹凸映射”,将UV减去位于距离后自然就呈现了纹理的效果

这样基本是可以了,再加一个strength变量吧,用于调节置换效果的强度,因为默认的强度可能会过于凸显置换纹理本身。

最终的效果图:

这么看来,我们得到了个比较有点感觉的转场特效。

题外话:上面的那些转场特效仅仅是以学习为目的的简单特效。推荐GitHub上的这个库——gl-transitions,里面有大量的社区实现的Shader转场效果可供参考和学习。

其实这种效果,如果是实装到HTML里面,我们或许是可以开发一个图片合成到视频的工具,也许就是简单版的web剪映(仅作为思考的问题产出)

最后修改:2024 年 08 月 14 日
收款不要了,给孩子补充点点赞数吧