今天看啥  ›  专栏  ›  佐笾

OpenGL ES入门:滤镜篇 - 漩涡、马赛克

佐笾  · 掘金  ·  · 2019-06-30 16:36
阅读 277

OpenGL ES入门:滤镜篇 - 漩涡、马赛克

系列推荐文章(基础篇):
OpenGL/OpenGL ES入门:图形API以及专业名词解析
OpenGL/OpenGL ES入门:渲染流程以及固定存储着色器
OpenGL/OpenGL ES入门:图像渲染实现以及渲染问题
OpenGL/OpenGL ES入门:基础变换 - 初识向量/矩阵
OpenGL/OpenGL ES入门:纹理初探 - 常用API解析
OpenGL/OpenGL ES入门: 纹理应用 - 纹理坐标及案例解析(金字塔)
OpenGL/OpenGL ES入门: 顶点着色器与片元着色器(OpenGL过渡OpenGL ES)
OpenGL/OpenGL ES入门: GLKit以及API简介
OpenGL/OpenGL ES入门: GLKit使用以及案例
OpenGL/OpenGL ES入门: 使用OpenGL ES 渲染图片
OpenGL/OpenGL ES入门:iOS纹理翻转策略解析
OpenGL ES入门: 渲染金字塔 - 颜色、纹理、纹理与颜色混合填充以及GLKit实现
OpenGL ES入门: 滤镜篇 - 分屏滤镜

和上一篇文章OpenGL ES入门: 滤镜篇 - 分屏滤镜一样,使用GLSL实现滤镜的前提条件是能够用GLSL显示普通图片

思路:

详细内容请参考OpenGL/OpenGL ES入门: 使用OpenGL ES 渲染图片

灰度滤镜GLSL 算法解析

图片的显示由三个颜色通道(rgb)显示的,而灰度滤镜只有一个值,也就是说只要得到亮度便可。下面提供5种方式实现灰度滤镜(前三种是利用权重来实现)

原理:

  • 浮点算法: Gray = R * 0.3 + G * 0.59 + B * 0.11
  • 整数算法: Gray = (R * 30 + G * 59 + B * 11) / 100
  • 移位算法: Gray = (R * 76 + G * 151 + B * 28) >> 8
  • 平均值法: Gray = (R + G + B) / 3;
  • 仅取绿色: Gray = G

片元着色器代码实现:

precision highp float;

uniform sampler2D Texture;
varying highp vec2 varyTextureCoord;
const highp vec3 W = vec3(0.2125, 0.7154, 0.0721); // 借用GPUImage的值

void main() {
    vec4 mask = texture2D(Texture, varyTextureCoord);
    float temp = dot(mask.rgb, W);
    gl_FragColor = vec4(vec3(temp), 1.0);
}
复制代码

漩涡滤镜GLSL 算法解析

把我最喜欢的女明星旋转成这样,真是罪过。。。

原理:
图形漩涡主要是在某个半径范围内,把当前采样点旋转一定角度,旋转以后当前点的颜色就被旋转后的点的颜色代替,因此整个半径范围里会有旋转的效果。 如果旋转的时候旋转角度随着当前点距离半径的距离递减,整个图像就会出现漩涡效果,如上图一样。 这里会使用抛物线递减因子: (1.0 - (r / Radius) * (r / Radius))

片元着色器代码

precision mediump float; 

uniform sampler2D Texture; 
//旋转⻆角度
const float uD = 80.0; 
//旋涡半径
const float uR = 0.5;
//纹理坐标
varying vec2 TextureCoordsVarying;

void main() {
    //获取旋转的直径
    float Res = float(512); 
    //纹理坐标[0,0],[1,0],[0,1],[1,1]...
    vec2 st = TextureCoordsVarying; 
    //半径 = 直径 * 0.5;
    float Radius = Res * uR;
    //准备旋转处理的纹理坐标 = 纹理坐标 * 直径 
    vec2 xy = Res * st;
   
    vec2 dxy = xy - vec2(Res/2.0, Res/2.0);
    //r
    float r = length(dxy);
    //抛物线递减因⼦子:(1.0-(r/Radius)*(r/Radius) )
    float beta = atan(dxy.y, dxy.x) + radians(uD) * 2.0 * (1.0-(r/Radius)*(r/Radius));
    if(r <= Radius)
    {
        //获取的纹理坐标旋转beta度.
        xy = vec2(Res/2.0, Res/2.0) + r*vec2(cos(beta), sin(beta));
    }
    //st = 旋转后的纹理坐标/旋转范围 
    st = xy/Res;
    //将旋转的纹理坐标替换原始纹理坐标TextureCoordsVarying 获取对应像素点的颜⾊. 
    vec3 irgb = texture2D(Texture, st).rgb;
    //将计算后的颜⾊填充到像素点中 gl_FragColor
    gl_FragColor = vec4( irgb, 1.0 );
}
复制代码

解析:
相比前面所说的滤镜,这个算法比较复杂,上面代码中,每一行也都有注释,但是,个人感觉看起来还是挺难懂的(可能笔者自己学的太差),所以这里把自己理解的方式叙述一下,如果有错误,欢迎大家指正。
上述代码中的Res,类似把一张图片分成了512个像素点一样来进行分析,实际上最后几步也通过xy/Res把它转换回来,所以下面就针对一个像素点或者说一个纹理来理解。

请先熟悉上面代码,至少知道每个变量的所代表的意义。

对于一个纹理来说:
vec2 xy = 1 * st 表示着纹理坐标,
vec2 dxy = xy - vec2(1.0/2.0, 1.0/2.0),通过下图来理解一下

通过上面图片来理解vec2 dxy = xy - vec2(1.0/2.0, 1.0/2.0)
然后通过length(),来取模,得到r

先忽略beta这一行,看if(r <= Radius),通过这个判断获取到的纹理像素,就是上图中深蓝色圆的纹素,所以最终形成的圆形漩涡就是通过这种方式来实现。
从这里也可以看出,如果你想控制圆形漩涡的半径或者圆形漩涡的位置都是可控的,即uR控制着圆的半径,向量vec2(Res/2.0, Res/2.0)控制着圆心位置,和Res无关,取决于分母2,有兴趣的小伙伴可以尝试一下。

下面来看角度beta的获取
当前角度:
atan(dxy.y, dxy.x)
加剧漩涡角度:
atan(dxy.y, dxy.x) + radians(uD) * 2.0
加剧漩涡衰减角度:
atan(dxy.y, dxy.x) + radians(uD) * 2.0 * (1.0-(r/Radius)*(r/Radius))

上图理解当前角度的计算。

获取的纹理坐标旋转beta度,看下图,为了方便,
假设(x0,y0)为未旋转之前的点,(x1,y1)为旋转之后的点,旋转角度为beta,长度即是模r不变.
那么r * vec2(cos(beta), sin(beta))便可理解为向量dx1y1,如果这里不清晰,可以尝试画个三角形,分别求出cos、sin的函数表达式,然后乘以模r,我相信应该很好理解,
所以vec2(Res/2.0, Res/2.0) + r*vec2(cos(beta), sin(beta))也就是最终的向量(x,y),如下图所示:

后面的代码也就是简单的获取纹理像素,然后填充到内建函数gl_FragColor中。

OK,有关漩涡滤镜实现原理的解析就到这里,如果大家发现描述有误,请及时提醒笔者改正,谢谢!!!

马赛克滤镜 GLSL 算法解析

原理:
马赛克效果就是把图片的一个相当大小的区域用同一个点的颜色来表示,可以认为是大规模的降低图像的分辨率,而让图片的一些细节隐藏起来。

矩形马赛克

片元着色器代码:

precision highp float;
// 纹理坐标
varying vec2 varyTextureCoord;
// 纹理采样器
uniform sampler2D Texture;
// 纹理图片Size
const vec2 TexSize = vec2(400.0, 400.0);
// 马赛克Size
const vec2 mosaicSize = vec2(10.0, 10.0);

void main () {
    vec2 intXY = vec2(varyTextureCoord.x * TexSize.x, varyTextureCoord.y * TexSize.y);
    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 = color;
}
复制代码

上面代码中,首先计算出intXY,表示实际图像位置; floor(x)内建函数,表示返回小于/等于x的最大整数值。 所以XYMosaic表示小马赛克的坐标,然后换算出马赛克的纹理坐标即可

六边形马赛克

片元着色器代码

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

const float mosaicSize = 0.03;

void main () {
    float length = mosaicSize;
    float TR = 0.866025;
    
    float x = TextureCoordsVarying.x;
    float y = TextureCoordsVarying.y;
    
    int wx = int(x / 1.5 / length);
    int wy = int(y / TR / length);
    vec2 v1, v2, vn;
    
    if (wx/2 * 2 == wx) {
        if (wy/2 * 2 == wy) {
            //(0,0),(1,1)
            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 {
            //(0,1),(1,0)
            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) {
            //(0,1),(1,0)
            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 {
            //(0,0),(1,1)
            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;
}
复制代码

解析:
我们需要做的效果就是让一张图片,分割成由六边形组成,让每个六边形中的颜色相同,下面采用直接取六边形中心点像素来实现

如上图,画出很多长和宽比例为3 :√3的矩形排列,然后对每一个点进行编号。 假如我们的屏幕的左上点为上图的(0,0)点,则屏幕上的任一点我们找到它所对应的那个矩形了了。

假定我们设定的矩阵比例为 3*LEN : √3*LEN ,那么屏幕上的任意 点(x, y)所对应的矩阵坐标为(int(x/(3*LEN)), int(y/ (√3*LEN)))

(wx, wy) 表示纹理坐标在所对应的矩阵坐标为:

wx = int(x/(1.5 * length))
wy = int(y/(TR * length))
复制代码

观察上图,标出了四个矩形,通过观察可以了解到,屏幕上的任意点(x, y),对应哪一个六边形,只要找到点,对应上面哪个矩形的关键点越近便可,而矩形坐标为(wx, wy),那么要判断(x, y)在那个矩形的关键点近取决于什么呢?

对于任何一个矩形,他们的四个顶点分别为:

  • 左上:vec2(length * 1.5 * float(wx), length * TR * float(wy));
  • 左下:vec2(length * 1.5 * float(wx), length * TR * float(wy + 1));
  • 右上:vec2(length * 1.5 * float(wx + 1), length * TR * float(wy));
  • 右下:vec2(length * 1.5 * float(wx + 1), length * TR * float(wy + 1));

上面片元着色器中的if判断,则是找到对应的那个矩形,然后找到关键的两个点v1,v2。 最后根据两点间距离公式,算出分别距离v1,v2的长度,距离那一个短,便属于那一个六边形。

文章写之不易,奈何数学功底有限,如有错误,希望大家指正,谢谢!!!

案例传送门:
github.com/SXDgit/Open…




原文地址:访问原文地址
快照地址: 访问文章快照