前言
Shadertoy是一个神奇的网站,有无数图形学大神分享的用shader写的各种让人匪夷所思的效果,而代码仅仅只有数十行至数百行。虽然很多实现无法直接应用到我们开发的实时渲染上,但分析它们的实现可以让我们理解shader以及带来很多灵感。
本文分析一下Shadertoy上一个比较通用的涡旋效果,也是个人非常喜欢的一个效果(主要是像写轮眼的神威),当然分析过程纯属个人猜测,不代表原作者思路,原文地址如下:
https://www.shadertoy.com/view/Ml2GDR
原效果如下:
正文
原文的实现除了扭曲外还加了光照和混合等,为了简化分析,我去掉了一些代码,如下:
void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
vec2 uv = fragCoord.xy/iResolution.xy-vec2(.5);
uv.y *= iResolution.y/iResolution.x;
float dist = length(uv);
float angle = atan(uv.y,uv.x);
uv = vec2(cos(angle+dist*3.),dist+(iTime*0.2));
fragColor = texture(iChannel0,uv);
}
另外为了看清特效如何实现,我换了纹理,得到的效果如下:
xuanwo.gif
第一步:纹理采样。
代码如下:
void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
vec2 uv = fragCoord.xy/iResolution.xy;
fragColor = texture(iChannel0,uv);
}
效果如下:
第一步先把纹理采样到屏幕上,可以看到一张没有任何扭曲效果的原图。这里需要注意是屏幕空间和纹理空间的概念。如下图,
代码中的iResolution代表屏幕分辨率(640, 360),fragCoord.xy/iResolution.xy得到的uv的范围就是(0, 1),也就是它在纹理空间中的坐标。
其实扭曲的原理就是,改变顶点坐标或者纹理坐标。因为Shadertoy中只有像素着色器,我们只能通过改变纹理坐标达到扭曲的效果。
第二步,求出当前像素点距离原点(0, 0)的距离,以及它与x轴的角度。
代码如下:
void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
vec2 uv = fragCoord.xy/iResolution.xy;
float dist = length(uv); //到原点(0,0)的距离
float angle = atan(uv.y, uv.x); //与x轴角度
fragColor = texture(iChannel0,uv);
}
第三步,改变纹理坐标
3.1 现在我们稍稍改变一下纹理坐标,看看画面会变成什么样子。
现在我们把采样坐标uv改成了uv = vec2(cos(angle),0.0)。画面瞬间变得面目全非。我们来仔细分析一下向量vec2(cos(angle), 0.0),我把uv的y置为0.0,什么意思呢?其实就是,在纹理空间中,限定了只在v==0.0的那条线上面进行采样。
那么uv的x,也就是cos(angle)意味着上面呢。意味着,所有在屏幕空间中,angle相同的坐标,它们的颜色是一样的!这就是为什么我们看到渲染的画面是一道道不同角度的直线组成的。如下图:
3.2 接下来我们再改变一下代码,把uv = vec2(cos(angle),0.0)换成uv = vec2(cos(angle + dist),0.0)。
画面会变成下面这样:
把原来的angle变成angle+dist,意味着,在所有angle+dist相同的点,它们的颜色是一样的,我们知道所有angle相同的点连起来是一条直线,那么所有angle+dist相同的点连起来是个什么形状呢?从渲染出来的画面我们看到是一条曲线,那为啥会是以这样的角度弯曲的曲线呢,看下图:
假设屏幕空间上有两点(图中的橙点和红点),红点的距离为dist1,角度为angle1,橙点的距离为dist2,角度为angle2。假设他们的dist1 + angle1 = dist2 + angle2,如果dist1>dist2,那么angle1就必然小于angle2,也就是距离越大,那么它的角度必然要越小,所以这些点组成的曲线必然是往右下方弯曲的!
3.3 我们把cos(angle+dist)改回cos(angle),同时把uv = vec2(cos(angle),0.0)的0.0改为uv.y
渲染出来的画面是这样的:
前面我们说过,uv = vec2(cos(angle), 0.0)表示在屏幕空间中所有angle相同的点的颜色都是相同的,该颜色就是在纹理空间中u==cos(angle),v==0.0的那一点的颜色。由此推出,uv = vec2(cos(angle), uv.y)意味着,在屏幕空间中所有angle相同的点的颜色,是对映射纹理空间中u==cos(angle),v==uv.v的颜色,如下图:
通过这个修改,屏幕空间中angle相同的直线上所有点的颜色不再是一个单一的颜色,而是采样了纹理贴图中的某一条u==cos(angle)的直线上的颜色,因此渲染出来的画面已经可以看出原图的内容了,只不过是被拉伸了。
3.4 把uv = vec2(cos(angle), uv.y)改成uv = vec2(cos(angle), dist)
渲染出来的画面是这样的:
那么,对于屏幕空间中angle的直线,仍然是采样纹理贴图中u==cos(angle)的直线上的颜色,只不过采样的步长变了,如下:
我们可以看到uv = vec2(cos(angle), dist)时渲染的画面没有uv = vec2(cos(angle), uv.y)拉伸的那么厉害,还需要注意的是,当uv == vec2(cos(angle), dist)的dist大于1.0时,采样坐标就超出了纹理贴图的范围,由于纹理是wrap设置的是repeat的,因此会重复采样。
3.5 把前面的效果都组合起来,将uv设为uv = vec2(cos(angle + dist * 3.), dist)
(dist *3中的3.是调整弯曲的程度)。得到下面的渲染效果:
第四步, “转”起来
对uv坐标加入时间变化因素,例如设为uv = vec2(cos(angle+dist3.), dist+(iTime0.2));那么对于每一个屏幕空间中的像素点,它们采样的纹理贴图的坐标不再是固定不变的,采样坐标的uv.v会随着iTime不断增大,超出1.0后会以wrap==repeat规则继续循环采样。可以看到下面的动效:
第五步,调整纹理坐标
5.1 渲染椭圆
现在我们有的是一个椭圆形的四分之一,如果我们想要渲染出完整的椭圆形该如何做呢?首先我们把扭曲效果先屏蔽,然后调整一下uv坐标,得到如下效果:
我们只在代码第一句,将uv坐标减去vec2(0.5, 0.5),它的效果是对采样坐标平移了(-0.5, -0.5),那么在纹理空间中采样的位置就会改变,如下:
蓝色虚线可以认为是我们的屏幕,我们看到屏幕的右上角四分之一采样的是纹理的左下角的四分之一,而其他四分之三并没有在纹理贴图(0,0)~(1,1)的范围,但是他们会以wrap==repeat的规则采样。
我们再把扭曲的效果加回来(把uv = vec2(cos(angle+dist3.),dist+(iTime0.2));前面的注释去掉),就可以得到如下动效:
5.2 渲染圆
现在的效果已经非常接近我们开头的效果了,只是开头呈现的旋涡是圆形的,而我们现在的是椭圆形的,这是由于屏幕的宽高比不等于1造成的。如果想要得到圆形的旋涡效果,只需要把uv的宽高逼调整一下即可,加入uv.y *= iResolution.y/iResolution.x;这句,得到
网友评论