GLSL应用-滤镜

2,280 阅读13分钟

       GLSL枯燥、乏味,可是它可以写出很多有趣的特效啊,比如现在各大小视频APP里的各种五花八万的滤镜。本篇将介绍几种简单的滤镜效果或者说是特效。这里就不在介绍GLSL代码如何在iOS中里利用了,不懂的同学可以去看我前面的文章

分屏滤镜

      假设我们的需求是分为上下两屏,取最中间(0.25-0.75)部分为内容.

     顶点着色器代码如下:

attribute vec4 position;
attribute vec2 textureCoord;
varying lowp vec2 varyingTextureCoord;

void main() {
    varyingTextureCoord = textureCoord;//将纹理坐标传到片段着色器
    gl_Position = position;//赋值顶点坐标
}

这段代码没什么好说的就是简单的将顶点坐标赋值给内置变量gl_Position。

     片元着色器代码如下:

precision highp float;
uniform sampler2D colorMap;
varying lowp vec2 varyingTextureCoord;

void main() {
    vec2 uv = varyingTextureCoord;
    if(uv.y < 0.5) {
        uv.y = uv.y + 0.25;
    } else {
        uv.y = uv.y - 0.25;
    }
    gl_FragColor = texture2D(colorMap, uv);
}

     我们知道内置函数texture2D的作用是根据纹理坐标提取文素(即提取相应纹理坐标对应的颜色),那么我们只需要将纹理分为上下两等分,并都提取中间部分(0.25至0.75部分)的文素即可。具体如下图:我们只需将虚线部分的内容分别填重到纹理的上下两部分即可满足需求。


      所以在提取文素时,只需将上半部分纹理(0-0.5)的纹理坐标纵坐标加上0.25即可,同理下半部分纹理在提取文素时将纹理坐标的纵坐标减去0.25即可。

      具体效果如下:左图为原图,右图为上下分屏后的效果。

 

灰度滤镜

      一般图片的显示取决于三个颜色通道(红、绿、蓝)。而灰度滤镜只需要一个亮度信息即可。因此,只需要将红绿蓝转化为灰度值即可。我们知道当红绿蓝三个颜色值相等的时候就是一个灰色,区别只是灰度值不同。知道这一点就好办了,比如我们可以取红绿蓝的平均值、甚至全部取绿色等等都可以达到灰度滤镜的效果。不过为了取得最好的视觉效果我们这里借鉴GPUImage中的权重值——红色占21.25%,绿色占71.54%,蓝色占7.21%。具体代码如下:

顶点着色器:

attribute vec4 position;
attribute vec2 textureCoord;
varying lowp vec2 varyingTextureCoord;

void main() {
    varyingTextureCoord = textureCoord;//将纹理坐标传到片段着色器
    gl_Position = position;//赋值顶点坐标
}

这里没什么可说的和上面的一样。

片元着色器:

precision highp float;
uniform sampler2D Texture;
varying vec2 varyingTextureCoord;
const highp vec3 W = vec3(0.2125, 0.7154, 0.0721);
void main (void) {
    
    vec4 mask = texture2D(Texture, varyingTextureCoord);
    float luminance = dot(mask.rgb, W);
    gl_FragColor = vec4(vec3(luminance), 1.0);
}

     这段代码先提取了原纹理的文素,然后用该文素点乘(红绿蓝分别乘以0.2125, 0.7154, 0.0721然后相加)权重向量作为灰度值。最后利用这个灰度值生成一个新的颜色复制给内置变量gl_FragColor。最终显示效果如下:

                   

      其实换成其他权重也应该是可以的,效果可能没有这么好而已 大家可以自行调整试试。三个权重值相加应该等于1.

浮雕滤镜

       浮雕效果是指图像的前景前向从而凸出背景。其基本原理是:根据像素与周围像素的差值确定浮雕图像的像素值,差别较大的像素(一般图像的边缘点像素差别较大)像素值较大,在灰度图中表现为较亮,边缘凸显,形成浮雕状,然后加上一个灰度偏移值,作为图片的整体底色。浮雕又分为八向浮雕和调和浮雕:

  •  八向浮雕:最基本的浮雕效果,根据像素值与周围八个方向的像素值的插值确定新的方向,常用于右下进行做差。
  • 调和浮雕:综合像素与左上、右上、左下、右下四个方向的差值确定新像素。

         相比较而言八向浮雕的立体效果更为明显,所以我们这里参考八向浮雕的原理。具体代码如下,顶点着色器和上面一样这里就不写了,直接看片元着色器。

precision highp float;,
uniform sampler2D Texture;
varying vec2 varyingTextureCoord;
const highp vec3 W = vec3(0.2125, 0.7154, 0.0721);//灰度权重
const vec2 TexSize = vec2(500.0, 500.0);//纹理大小
const vec4 bkColor = vec4(0.5, 0.5, 0.5, 1.0); //背景色

void main()
{
    vec2 tex = varyingTextureCoord;
    vec2 upLeftUV = vec2(tex.x-1.0/TexSize.x, tex.y-1.0/TexSize.y);//与左上做差
    vec4 curColor = texture2D(Texture, varyingTextureCoord);//当前像素点颜色
    vec4 upLeftColor = texture2D(Texture, upLeftUV);//左上方像素颜色
    vec4 delColor = curColor - upLeftColor;//和左上方的像素进行求差运算
    float luminance = dot(delColor.rgb, W);//转换为亮度信息,避免浮雕图像出现彩色像素
    gl_FragColor = vec4(vec3(luminance), 0.0) + bkColor;//加上一个灰度偏移作为背景色
}

      这里需要注意的是与左上做差,因为纹理默认是翻转的,在后面做纹理翻转的时候自然左上就变成了右下了。具体效果如下图:

                         

旋涡滤镜

       漩涡主要是在某个半径范围内,把当前采样点旋转 ⼀定的角度,旋转以后当前点的颜⾊色就被旋转后的点的颜色代替,因此整个半径范围内都会有旋转的效果。如果旋转的时候旋转⻆度随着当前点离圆心的距离递减,整个图像就会出现漩涡效果。这⾥使抛物线递减因⼦:(1.0-(r/Radius)*(r/Radius)),其中r为当前采样点距离圆心距离,Radius半径。

     具体代码如下。这里我们还是直接看片元着色器,顶点着色器和上面的一样。

precision highp float;
const float PI = 3.14159265;
uniform sampler2D Texture;
const float uD = 80.0;//表示旋转的角度
const float uR = 0.5;
varying vec2 varyingTextureCoord;

void main()
{
    ivec2 ires = ivec2(512, 512);//假设纹理由 512 * 512个像素点
    float Res = float(ires.y);//直径
    vec2 st = varyingTextureCoord;//纹理坐标(映射坐标)
    float Radius = Res * uR;//半径
    vec2 xy = Res * st;//在圆上任意采样点的实际纹理位置
    vec2 dxy = xy - vec2(Res/2., Res/2.);// 采样点到圆心的向量
    float r = length(dxy);//采样点到圆心的距离
    float attenValue = (1.0 -(r/Radius)*(r/Radius));//抛物线递减因子
    float beta = atan(dxy.y, dxy.x) + radians(uD) * 2.0 * attenValue; //旋转的角度
    if(r <= Radius)
    {
        xy = Res/2.0 + r * vec2(cos(beta), sin(beta));
    }
    st = xy/Res;//实际纹理位置转化成纹理坐标(0到1)
    vec3 irgb = texture2D(Texture, st).rgb;
    gl_FragColor = vec4( irgb, 1.0 );
}

        注意这里我们先假定纹理尺寸为512乘512,也可以假定成别的尺寸只要是个正方形就行,效果基本都是一样的。具体逻辑看下图:    


        上图中A点为纹理坐标的(0,0)点,B点为圆心(0.5,0.5),B点在圆上,AC为向量xy,AB为向量vec2(Res/2., Res/2.),所以AC-AB = BC.,所以BC表示dxy,其摸为r。因此,图中角度为atan(dxy.y, dxy.x)。所以当r在圆上时,r=半径,attenValue递减因子为0,角度不变,C点所在位置像素不会变化;当r往圆内移动时,attenValue递减因子开始变大,C点开始旋转偏移,当r变为0时候 偏移角度变到最大 。(感兴趣的同学可以将float beta = atan(dxy.y, dxy.x) + radians(uD) * 2.0 * attenValue这句改吃float beta = atan(dxy.y, dxy.x)看看是不是就是原图) 。

        因此当r<半径时,我们需要计算旋转后的像素所在纹理坐标,从而使用内建函数texture2D提取出新的文素。r * vec2(cos(beta), sin(beta))为,图中的dxy.x和dxy.y,  加上Res/2.0后正好转化为以A点为(0,0)纹理坐标,当然还需要除以Res转化为0到1的映射坐标。

       滤镜效果如下:

                   

马赛克

       ⻢赛克效果就是把图⽚的⼀个相当⼤小的区域⽤同⼀个点的颜⾊来填充.可以认为是大规模的降低图像的分辨率,⽽让图像的⼀些细节隐藏起来。

矩形马赛克

      矩形马赛克很简单,就是把纹理划分为一个个矩形,然后算出每个矩形的颜色即可。具体代码如下,同样直接看片元着色器:

precision highp float;

varying vec2 varyingTextureCoord;
uniform sampler2D Texture;
const vec2 TexSize = vec2(400.0, 400.0);
const vec2 mosaicSize = vec2(8.0, 8.0);

void main()
{
    vec2 intXY = vec2(varyingTextureCoord.x*TexSize.x, varyingTextureCoord.y*TexSize.y);//点(x,y)在纹理中的实际坐标
    
    vec2 XYMosaic = vec2(floor(intXY.x/mosaicSize.x)*mosaicSize.x, floor(intXY.y/mosaicSize.y)*mosaicSize.y);//点(x,y)所属哪一行哪一列的小矩形
    
    vec2 UVMosaic = vec2(XYMosaic.x/TexSize.x, XYMosaic.y/TexSize.y);//把该矩形看做一个大的像素点其纹理坐标就是UVMosaic
    vec4 color = texture2D(Texture, UVMosaic); //取出对应纹理坐标的文素
    gl_FragColor = color;
}

这段代码比较简单大家直接看注释就行,运行效果如下:

                  

六边形马赛克

        六边形马赛克就是把纹理划分为一个个六边形,然后同一个六边形内都取其中心点对应的文素填充。如下图:

      思路如下:取每个六边形的中心点画出⼀个矩阵,画出很多长和宽比例为 3:√3 的的矩形阵,假设屏幕的左上点为(0,0)点,则屏幕上的任⼀一点我们都可以找到它所对应的那个矩形了,知道了在哪个矩形那么就可以找到离他最近的那个六边形中心点了。如下图:              

具体代码如下,直接看片元着色器:

precision highp float;
uniform sampler2D Texture;
varying vec2 varyingTextureCoord;
const float mosaicSize = 0.02;
void main (void)
{
    float length = mosaicSize; // 设六边形马赛克边长为length
    float TR = sqrt(3.0)/2.0;
    float x = varyingTextureCoord.x;
    float y = varyingTextureCoord.y;
    int wx = int(x / 1.5 / length);
    int wy = int(y / TR / length); //纹理坐标为(x,y)的点对应的矩阵坐标为(wx,wy)
    vec2 v1, v2, vn;
    if (wx/2 * 2 == wx) {//偶数行
        if (wy/2 * 2 == wy) {//偶数列
            v1 = vec2(length * 1.5 * float(wx), length * TR * float(wy));
            v2 = vec2(length * 1.5 * float(wx + 1), length * TR * float(wy + 1));
        } else {//奇数列
            v1 = vec2(length * 1.5 * float(wx), length * TR * float(wy + 1));
            v2 = vec2(length * 1.5 * float(wx + 1), length * TR * float(wy));
        }
    }else {//奇数行
        if (wy/2 * 2 == wy) {//偶数列
            v1 = vec2(length * 1.5 * float(wx), length * TR * float(wy + 1));
            v2 = vec2(length * 1.5 * float(wx + 1), length * TR * float(wy));
        } else {//奇数列
            v1 = vec2(length * 1.5 * float(wx), length * TR * float(wy));
            v2 = vec2(length * 1.5 * float(wx + 1), length * TR * float(wy + 1));
        }
    }
    float s1 = sqrt(pow(v1.x - x, 2.0) + pow(v1.y - y, 2.0));
    float s2 = sqrt(pow(v2.x - x, 2.0) + pow(v2.y - y, 2.0));
    if (s1 < s2) {
        vn = v1;
    } else {
        vn = v2;
    }
    vec4 color = texture2D(Texture, vn);
    gl_FragColor = color;
    
}

     这段代码,有点小复杂,我们来捋一捋。首先我们链接各个六边形中心点,如上图那样讲整个纹理划分为一个个矩形,然后建立新的坐标系,坐标原点和刻度如上图。

     再看矩形的长宽比。如上图假设黄色箭头代表的长度为2个单位,由于图中标出的角度为30度,那么矩形的宽为√3,长为1+2 = 3.所以长宽比为3:√3.  所以设马赛克变成为length,则矩形宽为length* √3/2,矩形长为1.5*length。

      假设纹理上任意一点的纹理坐标为(x,y),(wx,wy)表示该点对应的矩阵坐标,即该点在第wx行,wy列所属的矩形区域内。

      确定点在哪个矩形区域后,分两种情况:


      第一种: 如上图表示当点在偶数行偶数列,或者奇数行奇数列的情形,这种情况下,只有左上右下有个点是六边形中心点。左上点的纹理坐标为:v1 = vec2(length * 1.5 * float(wx), length * TR * float(wy));右下点的纹理坐标为:v2 = vec2(length * 1.5 * float(wx + 1), length * TR * float(wy + 1));

     第二种:表示当点在奇数行偶数列,或者偶数行奇数列的情形,这种情况下,只有左下右上有个点是六边形中心点。左下点的纹理坐标为:v1 = vec2(length * 1.5 * float(wx), length * TR * float(wy + 1));右上点的纹理坐标为:v2 = v2 = vec2(length * 1.5 * float(wx + 1), length * TR * float(wy))。如下图:


      最后,我们只需要计算出这两种情况下点离其所在的矩形区域对应的2个六边形中心点的距离,然后比较距离较近的就是我们要取文素的点。

代码运行效果如下:

                       

三角形马赛克

      三角形马赛克是基于六边形的基础上的,具体原理如下,将六边形分割成留个三角形即可。


      具体思路是,先算出六个三角形的中心点纹理坐标,然后判断点在哪个三角形,最后取对应三角形中心点文素即可。

代码如下:

precision highp float;
uniform sampler2D Texture;
varying vec2 varyingTextureCoord;

float mosaicSize = 0.05;

void main (void){
    const float TR = sqrt(3.0)/2.0;
    const float PI6 = 0.523599; //30度,六分之一pi
    
    float x = varyingTextureCoord.x;
    float y = varyingTextureCoord.y;
    
    int wx = int(x/(1.5 * mosaicSize));
    int wy = int(y/(TR * mosaicSize));
    
    vec2 v1, v2, vn;
    
    if (wx / 2 * 2 == wx) {
        if (wy/2 * 2 == wy) {
            v1 = vec2(mosaicSize * 1.5 * float(wx), mosaicSize * TR * float(wy));
            v2 = vec2(mosaicSize * 1.5 * float(wx + 1), mosaicSize * TR * float(wy + 1));
        } else {
            v1 = vec2(mosaicSize * 1.5 * float(wx), mosaicSize * TR * float(wy + 1));
            v2 = vec2(mosaicSize * 1.5 * float(wx + 1), mosaicSize * TR * float(wy));
        }
    } else {
        if (wy/2 * 2 == wy) {
            v1 = vec2(mosaicSize * 1.5 * float(wx), mosaicSize * TR * float(wy + 1));
            v2 = vec2(mosaicSize * 1.5 * float(wx+1), mosaicSize * TR * float(wy));
        } else {
            v1 = vec2(mosaicSize * 1.5 * float(wx), mosaicSize * TR * float(wy));
            v2 = vec2(mosaicSize * 1.5 * float(wx + 1), mosaicSize * TR * float(wy+1));
        }
    }

    float s1 = sqrt(pow(v1.x - x, 2.0) + pow(v1.y - y, 2.0));
    float s2 = sqrt(pow(v2.x - x, 2.0) + pow(v2.y - y, 2.0));

    if (s1 < s2) {
        vn = v1;
    } else {
        vn = v2;
    }
    
    vec4 mid = texture2D(Texture, vn);
    float a = atan((y - vn.y),(x - vn.x));

    vec2 area1 = vec2(vn.x, vn.y - mosaicSize * TR / 2.0);
    vec2 area2 = vec2(vn.x + mosaicSize / 2.0, vn.y - mosaicSize * TR / 2.0);
    vec2 area3 = vec2(vn.x + mosaicSize / 2.0, vn.y + mosaicSize * TR / 2.0);
    vec2 area4 = vec2(vn.x, vn.y + mosaicSize * TR / 2.0);
    vec2 area5 = vec2(vn.x - mosaicSize / 2.0, vn.y + mosaicSize * TR / 2.0);
    vec2 area6 = vec2(vn.x - mosaicSize / 2.0, vn.y - mosaicSize * TR / 2.0);
    

    if (a >= PI6 * 2.0 && a < PI6 * 4.0) {
        vn = area1;
    } else if (a >= 0.0 && a < PI6 * 2.0) {
        vn = area2;
    } else if (a>= -PI6 * 2.0 && a < 0.0) {
        vn = area3;
    } else if (a >= -PI6 * 4.0 && a < -PI6 * 2.0) {
        vn = area4;
    } else if(a >= -PI6 * 6.0&& a < -PI6 * 4.0) {
        vn = area5;
    } else if (a >= PI6 * 4.0 && a < PI6 * 6.0) {
        vn = area6;
    }
    
    vec4 color = texture2D(Texture, vn);
    gl_FragColor = color;
}

     可以看到这段代码前面部分基本和六边形的一摸一样,后面新增了计算六个三角形的中心点纹理坐标,以及判断点所在的是哪个三角形区域的逻辑。

     这里需要注意的是用到了一个内置函数atan,这是一个计算角度的反正切函数,其参数有两种模式:atan(y, x)返回弧度 [-PI, PI]; atan(y/x):返回弧度 [-PI/2, PI/2]。我们这里需要选用 [-PI, PI]的函数模型。

    运行效果如下:为了效果更明显我们把马赛克size从0.03调整到了0.05,大家可以根据喜欢自行调整。

                         

圆形马赛克

       由于圆形好像无法将整个纹理铺满,所以效果不是那么好,这里我只提供个思路和着色器代码,感兴趣的可以留言。思路如下:将纹理分割成一个个正方形;然后在每个正方形内做内切圆;最后在每个正方形内判断点是否在园内,如果在园内就取圆形的文素,如果不在园内文素保持原图不变。代码如下:

precision highp float;

uniform sampler2D inputImageTexture;
varying vec2 varyingTextureCoord;
const vec2 TexSize = vec2(400.0, 400.0);
const vec2 mosaicSize = vec2(16.0, 16.0);

void main (void) {

    vec2 intXY = vec2(varyingTextureCoord.x*TexSize.x, varyingTextureCoord.y*TexSize.y);

    vec2 XYMosaic = vec2(floor(intXY.x/mosaicSize.x)*mosaicSize.x,floor(intXY.y/mosaicSize.y)*mosaicSize.y) + 0.5*mosaicSize;

    vec2 delXY = XYMosaic - intXY;
    float delL = length(delXY);
    vec2 UVMosaic = vec2(XYMosaic.x/TexSize.x,XYMosaic.y/TexSize.y);

    vec4 _finalColor;
    if(delL< 0.5*mosaicSize.x)
       _finalColor = texture2D(inputImageTexture,UVMosaic);
    else
       _finalColor = texture2D(inputImageTexture,varyingTextureCoord);

    gl_FragColor = _finalColor;
}

运行效果如下图:

                 

好像太长了,这一篇就到这里吧,下一篇继续。