OpenGL ES-案例实现灰度滤镜和马赛克滤镜

199 阅读7分钟

上一篇文章中主要讲了分屏滤镜,这篇我们了解一下灰度滤镜和马赛克滤镜,其中马赛克滤镜分别用正方形、六边形和三角形实现。 因为我们只是修改滤镜效果,所以UI逻辑和GLKit可以直接使用上一个案例的代码。并且因为顶点着色器代码不需要修改,我们只需要修改片元着色器文件即可。

原理及着色器代码

1、灰度滤镜

我们都知道,一般图片的每一个点颜色都是由RGB,即红绿蓝三种颜色混合的到的,每一种颜色即称为一个颜色通道,所以一般的图片也可以叫做三通道图。那么灰度滤镜其实就是只有一个通道有值,也就是只要得到图片的亮度即可,其实这也就是单通道图。

单通道图:俗称灰度图,每个像素点只能有有一个值表示颜色,它的像素值在0到255之间,0是黑色,255是白色,中间值是一些不同等级的灰色。(也有3通道的灰度图,3通道灰度图只有一个通道有值,其他两个通道的值都是零)。一般都取绿色通道单通道,因为人的眼睛对绿色的反应是最强烈的,这是一种生理反应。 灰度滤镜的常用算法一共有5种,大致分为3类:

  • 权值法:处理后的图片比较逼真 浮点算法:Gray = R*0.3 + G*0.59 + B*0.11 (RGB的权重总和为1) 整数方法:Gray = (R*30 + G*59 + B*11)/100(RGB的权重总和为100) 移位方法:Gray = (R*76 + G*151 + B*28)>>8
  • 平均值法:Gray = (R+G+B)/3,处理后的图片比较柔和
  • 仅取绿色:Gray = G,这种方式方便简单,且易用

接下来我们分别使用浮点算法仅取绿色实现灰度滤镜算法

浮点算法代码:

precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
//RGB的变换因子,即权重值
const highp vec3 W = vec3(0.2125, 0.7154, 0.0721);

void main(){
    //获取对应纹理坐标系下色颜色值
    vec4 mask = texture2D(Texture, TextureCoordsVarying);
    //将颜色mask 与 变换因子相乘得到灰度值
    float luminance = dot(mask.rgb, W);
    //将灰度值转换为(luminance,luminance,luminance,mask.a)并填充到像素中
    gl_FragColor = vec4(vec3(luminance), 1.0);
}

仅取绿色值:

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

void main(){
    //获取对应纹理坐标系下色颜色值
    vec4 mask = texture2D(Texture, TextureCoordsVarying);
    
    //将RGB全部设置为G,即GRB全部取绿色值
    gl_FragColor = vec4(mask.g, mask.g, mask.g, 1.0);
}

注意:此处增加注释只是为了方便阅读和理解,实际开发的时候尽量不要在着色器代码中添加注释,防止出现编译错误。

我们来看下两者的结果:

灰度滤镜对比.png

左边是浮点算法,右边是取绿色值,可以看出右边会更偏绿一点。

2、颠倒滤镜

看过我之前关于纹理翻转的文章的同学应该知道怎么实现了吧,超级简单,只需要将片源着色器中纹理的方向根据你想要颠倒的方向取反。我们演示的是上下颠倒,所以对纹理的Y坐标取反即可。我们看下代码:

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

void main (void) {
    vec4 color = texture2D(Texture, vec2(TextureCoordsVarying.x, 1.0 - TextureCoordsVarying.y));
    gl_FragColor = color;
}

效果图: 颠倒.png

3、马赛克滤镜

⻢赛克效果就是把图片的⼀个相当⼤小的区域⽤同一个点的颜色来表示.可以认为是大规模的降低图像的分辨率,而让图像的一些细节隐藏起来。 所以我们只要根据马赛克单元格的宽高计算出图像总的马赛克行数和列数,然后将每个马赛克单元格遍历2次,第一次计算该单元格RGB的平均值,第二次遍历赋颜色值。 image.png 代码实现:

precision mediump float;
//纹理坐标
varying vec2 TextureCoordsVarying;
//纹理采样器
uniform sampler2D Texture;
//纹理图片size
const vec2 TexSize = vec2(500.0, 500.0);
//马赛克size
const vec2 mosaicSize = vec2(16.0, 16.0);

void main()
{
    //计算图像的实际位置
    vec2 intXY = vec2(TextureCoordsVarying.x*TexSize.x, TextureCoordsVarying.y*TexSize.y);
    // floor (x) 内建函数,返回小于/等于X的最大整数值.
    // floor (intXY.x / mosaicSize.x) * mosaicSize.x 计算出一个⼩小⻢赛克的坐标.
    vec2 XYMosaic = vec2(floor(intXY.x/mosaicSize.x)*mosaicSize.x, floor(intXY.y/mosaicSize.y)*mosaicSize.y);
    //换算回纹理坐标
    vec2 UVMosaic = vec2(XYMosaic.x/TexSize.x, XYMosaic.y/TexSize.y);
    //获取到马赛克后的纹理坐标的颜色值
    vec4 color = texture2D(Texture, UVMosaic);
    //将⻢赛克颜色值赋值给gl_FragColor. 
    gl_FragColor = color;
}

效果图: 正方形马赛克1.png 如果觉得太粗糙了也可以将马赛克size改小一点:

const vec2 mosaicSize = vec2(8.0, 8.0);

效果图: 正方形马赛克2.png

4、六边形马赛克

和正方形类似,我们是将马赛克显示的图形从正方形换到了六边形,我们要提取的马赛克颜色实际上就从每一个六边形中提取,默认我们是选择六边形的中心点颜色作为最终马赛克的显示颜色。如下图: 分割成六边形.png

我们提取出其中一部分放大看,就会发现实际上每一个六边形都可以由四个矩形组成。 4个矩形.png

由基本的数据知识,我们可以知道这个矩阵的长宽比为3:√3,假如我们的屏幕的左上点为上图的(0,0)点,则屏幕上的任一点我 们找到它所对应的那个矩形。 假定我们设定的矩阵比例为 3*length : √3*length ,那么屏幕上的任意 点(x, y)所对应的矩阵坐标为(int(x/(3*length)), int(y/ (√3*length)))。那么(wx, wy) 表示纹理坐标在所对应的矩阵坐标为: wx = int(x/(1.5 * length)); wy = int(y/(TR * length))

我们再观察下4个矩阵,实际上他们只有以下两种类型: 两种类型的矩形.png 所以我们也可以计算出每个矩形的4个顶点的坐标: 坐标.png

每一种类型中,也只包含两个六边形的中心点,所以我们在计算选择哪个点颜色时候,只需要判断这个点离哪个六边形的中心点近,然后选后近的那个中心点并取其对应的颜色即可: 根据距离公式求像素点距离两个中心点的距离D1、D2 D1 = √((v1.x-x)² + (v1.y-y)²) D2 = √((v2.x-x)² + (v2.y-y)²)

那我们怎么判断是哪两个中心点呢?再来看一下上面的分割成六边形.png,在划分六边形的时候已经对分割的区域进行了行列标号,

  • 偶数行偶数列计算左上中心点和右下中心点即包含六边形的边为/
  • 偶数行奇数列计算左下中心点和右上中心点即包含六边形的边为\
  • 奇数行偶数列计算左下中心点和右上中心点即包含六边形的边为\
  • 奇数行奇数列计算左上中心点和右下中心点即包含六边形的边为/

接下来看下代码:

precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
//六边形的边长
const float mosaicSize = 0.03;

void main(){
    
    float length = mosaicSize;
    //矩形的高的比例为√3,取值 √3/2 ,也可以直接取√3
    float TR = 0.866025;
    //矩形的长的比例为3,取值 3/2 = 1.5,也可以直接取3
    float TB = 1.5;
    
    //取出纹理坐标
    float x = TextureCoordsVarying.x;
    float y = TextureCoordsVarying.y;
    
    //根据纹理坐标计算出对应的矩阵坐标 
    //即 矩阵坐标wx = int(纹理坐标x/ 矩阵长),矩阵长 = TB*len
    //即 矩阵坐标wy = int(纹理坐标y/ 矩阵宽),矩阵宽 = TR*len
    int wx = int(x / TB / length);
    int wy = int(y / TR / length);
    vec2 v1, v2, vn;
    
    //判断wx是否为偶数,等价于 wx % 2 == 0
    if (wx/2 * 2 == wx) {
        if (wy/2 * 2 == wy) {//偶行偶列
            //(0,0),(1,1)
            v1 = vec2(length * TB * float(wx), length * TR * float(wy));
            v2 = vec2(length * TB * float(wx+1), length * TR * float(wy+1));
        }else{//偶行奇列
            //(0,1),(1,0)
            v1 = vec2(length * TB * float(wx), length * TR * float(wy+1));
            v2 = vec2(length * TB * float(wx+1), length * TR * float(wy));
        }
    }else{
        if (wy/2 * 2 == wy) {//奇行偶列
            //(0,1),(1,0)
            v1 = vec2(length * TB * float(wx), length * TR * float(wy+1));
            v2 = vec2(length * TB * float(wx+1), length * TR * float(wy));
        }else{//奇行奇列
            //(0,0),(1,1)
            v1 = vec2(length * TB * float(wx), length * TR * float(wy));
            v2 = vec2(length * TB * 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));
    
    //选择距离小的则为六边形的中心点,且获取它的颜色
    vn = (s1 < s2) ? v1 : v2;
    //获取六边形中心点的颜色值
    vec4 color = texture2D(Texture, vn);
    gl_FragColor = color;
}

效果如下: 六边形马赛克1.png 同样的可已通过修改六边形的边长来控制六边形的大小。比如:

const float mosaicSize = 0.01;

效果如下: 六边形马赛克2.png

5、三角形马赛克

三角形马赛克实际上就是从六边形马赛克基础上演变而来的,一个六边形可以拆分为6个三角形: 三角形马赛克.png 结合六边形的逻辑,我们在取到对应的六边形的中心点后,再获取到该像素点和六边形中心点的夹角: 假设点的坐标为:(x,y) 六边形中心点为:Vn(x,y) 计算夹角.png 注意夹角的范围是[-180,180],即[-π,π],再根据三角形马赛克.png即可获取到所在的三角形,然后再获取该三角形中心点的颜色,并赋值给三角形即可。 计算三角形中心点: 计算三角形中心点.png

着色器代码:

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

float mosaicSize = 0.03;

void main (void){
    
    float length = mosaicSize;
    //矩形的高的比例为√3,取值 √3/2 ,也可以直接取√3
    float TR = 0.866025;
    //矩形的长的比例为3,取值 3/2 = 1.5,也可以直接取3
    float TB = 1.5;
    
    //取出纹理坐标
    float x = TextureCoordsVarying.x;
    float y = TextureCoordsVarying.y;
    
    //根据纹理坐标计算出对应的矩阵坐标 
    //即 矩阵坐标wx = int(纹理坐标x/ 矩阵长),矩阵长 = TB*len
    //即 矩阵坐标wy = int(纹理坐标y/ 矩阵宽),矩阵宽 = TR*len
    int wx = int(x / TB / length);
    int wy = int(y / TR / length);
    vec2 v1, v2, vn;
    
    //判断wx是否为偶数,等价于 wx % 2 == 0
    if (wx/2 * 2 == wx) {
        if (wy/2 * 2 == wy) {//偶行偶列
            //(0,0),(1,1)
            v1 = vec2(length * TB * float(wx), length * TR * float(wy));
            v2 = vec2(length * TB * float(wx+1), length * TR * float(wy+1));
        }else{//偶行奇列
            //(0,1),(1,0)
            v1 = vec2(length * TB * float(wx), length * TR * float(wy+1));
            v2 = vec2(length * TB * float(wx+1), length * TR * float(wy));
        }
    }else{
        if (wy/2 * 2 == wy) {//奇行偶列
            //(0,1),(1,0)
            v1 = vec2(length * TB * float(wx), length * TR * float(wy+1));
            v2 = vec2(length * TB * float(wx+1), length * TR * float(wy));
        }else{//奇行奇列
            //(0,0),(1,1)
            v1 = vec2(length * TB * float(wx), length * TR * float(wy));
            v2 = vec2(length * TB * 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));
    
    //选择距离小的则为六边形的中心点,且获取它的颜色
    vn = (s1 < s2) ? v1 : v2;
    //获取六边形中心点的颜色值
    vec4 mid = texture2D(Texture, vn);

    //获取像素点与中心点的角度
   float a = atan((x-vn.x)/(y-vn.y));
    
    //判断夹角,属于哪个三角形,则获取哪个三角形的中心点坐标
    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 && a < PI6 * 3.0) {
        vn = area1;
    }else if (a >= PI6 * 3.0 && a < PI6 * 5.0){
        vn = area2;
    }else if ((a >= PI6 * 5.0 && a <= PI6 * 6.0) || (a < -PI6 * 5.0 && a > -PI6 * 6.0)){
        vn = area3;
    }else if (a < -PI6 * 3.0 && a >= -PI6 * 5.0){
        vn = area4;
    }else if (a <= -PI6 && a > -PI6 * 3.0){
        vn = area5;
    }else if (a > -PI6 && a < PI6){
        vn = area6;
    }
    //获取对应三角形重心的颜色值
    vec4 color = texture2D(Texture, vn);
    // 将颜色值填充到片元着色器内置变量gl_FragColor
    gl_FragColor = color;

}

效果如下: 三角形马赛克1.png 同样也可以修改通过修改六边形的边长来修改三角形的大小:

const float mosaicSize = 0.01;

效果如下: 三角形马赛克2.png

觉得不错记得点赞哦!听说看完点赞的人逢考必过,逢奖必中。ღ( ´・ᴗ・` )比心