作为一个前端开发的人员,基本上离不开一个用来显示图片的标签,那就是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,步骤如下:
- 点击右下角的黑框
iChannel0
- 点击“纹理”,然后从下面的图片列表中随便选一张即可,选完后点击右上方的叉叉关闭。
接下来,引入了纹理,我们为了将它显示在屏幕上,要进行的就是采样操作,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剪映(仅作为思考的问题产出)