一看就懂的OpenGL ES教程——仿抖音滤镜的各种奇技淫巧之基础滤镜

3,417 阅读12分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

通过阅读本文,你将获得以下收获:
1.通过Shader实现滤镜的基本原理
2.如何实现静态滤镜
3.常见的静态滤镜实例分析

上篇回顾

上一篇文章一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年 已经详细阐述了如何用OpenGL es将原始的YUV数据组成的视频渲染到屏幕上,想必有很多童鞋在阅读了它之后依然觉得回味无穷,学习的胃口也越来越大了,因为你们知道仅仅渲染视频是不够的,我们要的是,能够在视频上面玩出花来,于是,在本系列已经渐入高潮的上一篇文章之后,我将在本文掀起更大的“浪花”,进一步满足列位看官的求知欲。本文将讲解使用OpenGL es给视频添加各种放抖音滤镜特效,这也是我在本系列第一篇文章中承诺过的。

1e6b5cd09310b0bdb7c85615385881cc.jpeg

说起滤镜,这是这个年头一个很火很神奇的东西,几乎可以说能化腐朽为神奇(化野兽为美女?)都不过分。最初是在photoshop这类图像处理软件中逐步为世人所知,后来被抖音这类视频软件玩出花来,为世人特别是花季少女所倾心~

image.png

那滤镜的实现原理又是怎样的呢?

滤镜基本原理

其实我们在本系列之前的文章已经有接触过这方面的东西,还记得这张图么:

image.png

嘻嘻,你又怎么会不记得呢~~ 这图就是在一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(实践篇)最后做图层混合的示例图,还记得我们怎么实现的么?

回顾一下当时的片段着色器:

               #version 300 es
                precision mediump float;
                in vec2 TexCoord;
                out vec4 FragColor;
                //传入的纹理
                uniform sampler2D ourTexture;
                //新增纹理单元
                uniform sampler2D ourTexture1;

                void main() {
                    //对2个纹理进行混合
                    FragColor = mix(texture(ourTexture, TexCoord), texture(ourTexture1, TexCoord), 0.5);
                };

通过2个纹理单元去采样2张图片,对当前片段采样到的颜色值按照0.5透明度的比例进行混合,即颜色值1的0.5透明度+颜色值2的0.5透明度。

所以我们可以得出结论,滤镜的实现,就是在片段着色器采样的时候,我们不再老老实实将采样得到的颜色值直接赋给当前片段的最终颜色值,而是增加一些额外的处理,最后再赋给当前片段的最终颜色值。

至于什么样的处理,那就需要好好发挥我们的想象力和算法功底了。

54773cd893f88e756f8d0591085536e0.jpeg

下面我将用几个代表性的初级视频滤镜实例来讲解,因为学习都是一个循序渐进的过程,所以对滤镜实现的具体讲解,我也将分为从易到难的级别逐步讲解。

值得注意的是,所用视频依旧是大家最喜爱的《龙猫》哦~所以代码全部基于上一篇的代码,改的主要是片段着色部分。

滤镜升级打怪

青铜

首先是青铜级别,即最简单的级别,也就是对每个片段都是一样的处理方式

灰度滤镜

test1.gif

顾名思义,就是将每一帧画面转化为灰度图。灰度就是没有色彩,对于RGB来说,即色彩分量全部相等,对于YUV来说,即UV分量都为默认值128。

这一点从RGB和YUV的互相转化公式就能推出来,以下为BT601的Limited Range的转化公式:

image.png

对于RGB转YUV的公式来说,假如RGB三个分量都相等,那么可以算出此时Y等于RGB中任意一个分量的值,而U、V都是128。

而对于YUV转RGB的公式来说,当YUV的U、V都为128的时候,R、G、B都等于Y。

基于此,片段着色器代码就不难写了。

第一种是最简单的,就是直接将采样到的YUV中的UV置为默认值,即无色的值

        #version 300 es
        precision mediump float;
        //纹理坐标
        in vec2 vTextCoord;
        //输入的yuv三个纹理
        uniform sampler2D yTexture;//采样器
        uniform sampler2D uTexture;//采样器
        uniform sampler2D vTexture;//采样器
        out vec4 FragColor;
        void main() {
           //采样到的yuv向量数据
           vec3 yuv;
           //yuv转化得到的rgb向量数据
           vec3 rgb;
           //分别取yuv各个分量的采样纹理
           yuv.x = texture(yTexture, vTextCoord).r;
           //直接将uv置为0.0即可(0.5-0.5)
           yuv.y = 0.0;
           yuv.z = 0.0;
           rgb = mat3(
                    1.0, 1.0, 1.0,
                    0.0, -0.183, 1.816,
                    1.540, -0.459, 0.0
            ) * yuv;         
             FragColor = vec4(rgb, 1.0);
         };

由于在OpenGL es内部已经做了归一化,所以采样到的YUV数值是从0.0-1.0的,因为UV每个通道的范围为0至255,因而此时的无色对应的就是128(更细致地来说是色度分量在偏置处理前的取值范围是-128至127,这时候的无色对应的是“0”值。经过偏置后色度分量取值变成了0至255,因而此时的无色对应的就是128了),对应在shader中归一化的数值就是0.5,又因为在矩阵相乘之前先减0.5,所以这里就直接置为0.0即可

第二种,使用灰度转化公式

gray = R * 0.2125 + G * 0.7154 + B * 0.0721

直接代入,没啥好说的,不要问我公式怎么推导的。

        #version 300 es
        precision mediump float;
        //纹理坐标
        in vec2 vTextCoord;
        //输入的yuv三个纹理
        uniform sampler2D yTexture;//采样器
        uniform sampler2D uTexture;//采样器
        uniform sampler2D vTexture;//采样器
        out vec4 FragColor;
        void main() {
           //采样到的yuv向量数据
           vec3 yuv;
           //yuv转化得到的rgb向量数据
           vec3 rgb;
           //分别取yuv各个分量的采样纹理
           yuv.x = texture(yTexture, vTextCoord).r;
           //直接将uv置为0.0即可(0.5-0.5)
           yuv.y = 0.0;
           yuv.z = 0.0;
           rgb = mat3(
                    1.0, 1.0, 1.0,
                    0.0, -0.183, 1.816,
                    1.540, -0.459, 0.0
            ) * yuv;  
            float gray = rgb.r * 0.2125 + rgb.g * 0.7154 + rgb.b * 0.0721;
             FragColor = vec4(gray,gray,gray, 1.0);
         };

几行代码功夫,萌萌的动画片变得特别有时代感~

20a0ad9e1f86edbf380995221bc9c726.jpeg

反色滤镜

test2.gif

所谓的反色,就是其RGB颜色值与其相加和为255的对应颜色值,将一幅图置为反色,看起来就有种拍X光的效果。常见的反色如下表格所示,左右项互为反色(来源于百度百科):

白色(255,255,255)黑色(0,0,0)
灰色-25%(195,195,195)灰度-80%(60,60,60)
褐色(185,122,87)深灰蓝绿色(70,133,168)
玫瑰色(粉红)(255,174,201)深绿色(0,81,54)
金色(255,201,14)蓝色(0,54,241)
浅黄色(239,228,176)墨蓝色(16,27,79)
酸橙色(181,230,29)亮蓝色(74,25,226)
淡青绿色(153,217,234)深红褐色(102,38,21)
蓝灰色(112,146,190)咖啡色(143,109,65)
淡蓝紫色(200,191,231)深苔藓色(55,64,24)
黑色(0,0,0)白色(255,255,255)
灰色-50%(127,127,127)灰色-50%(127,127,127)(反色就是它本身)
深红色(136,0,21)浅蓝绿色(119,255,234)
红色(237,28,36)蓝绿色(18,227,219)
橙色(255,127,39)暗青蓝色(0,128,216)
黄色(255,242,0)靛蓝色(0,13,255)
绿色(34,177,76)暗玫红色(221,78,179)
青绿(0,162,232)鲜橙色(255,93,23)
靛青(63,72,204)棕黄色(192,183,51)
紫色(163,73,164)草绿色(92,182,91)

这样片段着色器就很简单了:

        #version 300 es
        precision mediump float;
        //纹理坐标
        in vec2 vTextCoord;
        //输入的yuv三个纹理
        uniform sampler2D yTexture;//采样器
        uniform sampler2D uTexture;//采样器
        uniform sampler2D vTexture;//采样器
        out vec4 FragColor;
        void main() {
           //采样到的yuv向量数据
           vec3 yuv;
           //yuv转化得到的rgb向量数据
           vec3 rgb;
           //分别取yuv各个分量的采样纹理
           yuv.x = texture(yTexture, vTextCoord).r;
           //直接将uv置为0.0即可(0.5-0.5)
           yuv.y = 0.0;
           yuv.z = 0.0;
           rgb = mat3(
                    1.0, 1.0, 1.0,
                    0.0, -0.183, 1.816,
                    1.540, -0.459, 0.0
            ) * yuv;  
            //取反色
            FragColor = vec4(vec3(1.0 - rgb.r, 1.0 - rgb.g, 1.0 - rgb.b), 1.0);

         };

只要最后赋值的一行改为以下即可:

 FragColor = vec4(vec3(1.0 - rgb.r, 1.0 - rgb.g, 1.0 - rgb.b), 1.0);

一行代码功夫,一切都”反了“。

b0ce8bb4b314a4e420cb4e029358a8a3.jpeg

白银

白银级别难度当然有所提升,主要是不同区域的片段的处理方式不一样了。

灰度反色交叉滤镜

test5.gif

        #version 300 es
        precision mediump float;
        //纹理坐标
        in vec2 vTextCoord;
        //输入的yuv三个纹理
        uniform sampler2D yTexture;//采样器
        uniform sampler2D uTexture;//采样器
        uniform sampler2D vTexture;//采样器
        out vec4 FragColor;
     
        void main() {
           //采样到的yuv向量数据
           vec3 yuv;
           //yuv转化得到的rgb向量数据
           vec3 rgb;
           //分别取yuv各个分量的采样纹理(r表示?)
           yuv.x = texture(yTexture, vTextCoord).r;
           yuv.y = texture(uTexture, vTextCoord).g - 0.5;
           yuv.z = texture(vTexture, vTextCoord).b - 0.5;
           rgb = mat3(
                    1.0, 1.0, 1.0,
                    0.0, -0.183, 1.816,
                    1.540, -0.459, 0.0
            ) * yuv;
            //根据不同的纹理坐标区域,赋值不同颜色值给当前当前片段颜色值
            if (vTextCoord.x < 0.5 && vTextCoord.y < 0.5) {
                //左上角区域,反色滤镜
                FragColor = vec4(vec3(1.0 - rgb.r, 1.0 - rgb.g, 1.0 - rgb.b), 1.0);
            } else if (vTextCoord.x > 0.5 && vTextCoord.y > 0.5) {
                //右下角区域,灰度滤镜
                float gray = rgb.r * 0.2125 + rgb.g * 0.7154 + rgb.b * 0.0721;
                FragColor = vec4(gray, gray, gray, 1.0);
            } else {
                FragColor = vec4(rgb, 1.0);        
            }
         };

代码一出来,其实也是so easy~关键点就是对于纹理坐标所在区域的判断,如果处于左上角,即x<0.5,y<0.5,则使用反色效果。如果处于右下角,即x>0.5,y>0.5,则使用灰度效果。其余区域不做额外处理

又是几行代码的功夫,就戴上了”有色眼镜“~

e37cfd8bacf42f638a4b733e5d8e3700.jpeg

黄金

黄金级别对于刚接触的童鞋来说可能是一个小门槛,因为这里开始当前片段采样的纹素可能并非是片段本身对应的纹理坐标了,而是根据需要采样自己想要的纹理坐标位置的颜色值

二分屏

test3.gif

二分屏,顾名思义,即将一个画面分为2个重复的画面在平均分的屏幕位置上渲染。这里的二分屏为了保证图像不变形,所以每个分屏都采样原来纹理图片的中间一半的区域。如下图所示,左边是被渲染的图元,右边是被采样的纹理:

image.png

该图显示的是针对片段在第一个分屏的情况,因为之前讲过纹理映射就是相当于将图元的顶点和纹理的顶点一一对上。

从这个图我们可以得出一个通用结论,假如当前片段坐标为(x,y),当y小于0.5的时候,则采样纹理图片对应位置为y+0.25的纹素

那么对于下方的分屏,就可以顺藤摸瓜推出以下结论:

当y大于0.5的时候,则采样纹理图片对应位置为y-0.25的纹素。

上片段着色器代码:

#version 300 es

precision mediump float;
//纹理坐标
in vec2 vTextCoord;
//输入的yuv三个纹理
uniform sampler2D yTexture;//采样器
uniform sampler2D uTexture;//采样器
uniform sampler2D vTexture;//采样器
out vec4 FragColor;
void main() {
    //采样到的yuv向量数据
    vec3 yuv;
    //yuv转化得到的rgb向量数据
    vec3 rgb;
   
    vec2 uv = vTextCoord.xy;
    float y;
    //关键点,对渲染图元不同位置的点采样纹理的不同位置
    if (uv.y >= 0.0 && uv.y <= 0.5) {
        //当渲染图元的点位于上半部分的时候,采样比其纵坐标大于0.25部分
        uv.y = uv.y + 0.25;
    }else{
        //当渲染图元的点位于下半部分的时候,采样比其纵坐标小于0.25部分
        uv.y = uv.y - 0.25;
    }
    //分别取yuv各个分量的采样纹理
    yuv.x = texture(yTexture, uv).r;
    yuv.y = texture(uTexture, uv).g - 0.5;
    yuv.z = texture(vTexture, uv).b - 0.5;
    rgb = mat3(
            1.0, 1.0, 1.0,
            0.0, -0.183, 1.816,
            1.540, -0.459, 0.0
    ) * yuv;
    FragColor = vec4(rgb, 1.0);
 };

四分屏

test4.gif

四分屏就更好玩了,但是原理和二分屏是一样的。

如下图,左边是被渲染的图元,右边是被采样的纹理,像之前所说的,可以将纹理和图元的顶点一一对上,比如四分屏的第一个格子和纹理的对应关系如下图所示:

image.png

所以对于第一个分屏,假如此时需要渲染的片段为(x,y),则可以推出采样的通用关系:

当x<0.5,y<0.5的时候,采样(x*2,y*2)的纹素。

那么以此类推,就可以推出:

当x>0.5,y<0.5的时候,采样((x-0.5)*2,y*2)的纹素。

当x<0.5,y>0.5的时候,采样(x*2,(y-0.5)*2)的纹素。

当x>0.5,y>0.5的时候,采样((x-0.5)*2,(y-0.5)*2)的纹素。

片段着色器代码:

#version 300 es

precision mediump float;
//纹理坐标
in vec2 vTextCoord;
//输入的yuv三个纹理
uniform sampler2D yTexture;//采样器
uniform sampler2D uTexture;//采样器
uniform sampler2D vTexture;//采样器
out vec4 FragColor;
void main() {
    //采样到的yuv向量数据
    vec3 yuv;
    //yuv转化得到的rgb向量数据
    vec3 rgb;

    vec2 uv = vTextCoord.xy;
    if (uv.x <= 0.5) {
        //当x小于0.5的时候,采样2倍x坐标的纹素颜色
        uv.x = uv.x * 2.0;
    }else{
        //当x大于0.5的时候,采样2倍x坐标减0.5的纹素颜色
        uv.x = (uv.x - 0.5) * 2.0;
    }
   
     if (uv.y <= 0.5) {
           //当y小于0.5的时候,采样2倍y坐标的纹素颜色  
           uv.y = uv.y * 2.0;
     }else{
           //当y大于0.5的时候,采样2倍y坐标减0.5的纹素颜色
           uv.y = (uv.y - 0.5) * 2.0;
     }
    //分别取yuv各个分量的采样纹理
    yuv.x = texture(yTexture, uv).r;
    yuv.y = texture(uTexture, uv).g - 0.5;
    yuv.z = texture(vTexture, uv).b - 0.5;
    rgb = mat3(
            1.0, 1.0, 1.0,
            0.0, -0.183, 1.816,
            1.540, -0.459, 0.0
    ) * yuv;
    FragColor = vec4(rgb, 1.0);
 };

这么一看也没什么神奇的,你说呢?

a57cde87223d464c3605c51e673d574d.jpeg

总结

本文详细叙述了几种常见的滤镜效果实现原理,让广大的程序员也有机会体验做一把画家艺术家的快感,当然段位仅仅进行到了黄金级别未免显得太菜鸡了吧,所以下一篇文章才是真正冲击王者宝座的时机 一看就懂的OpenGL ES教程——仿抖音滤镜的奇技淫巧之变换滤镜(理论基础篇)

欢迎点赞加关注,让我们一起早日上王者段位~

aceefaede6d2d19d49e50b1b58844512.jpeg

项目代码

opengl-es-study-demo 不断更新中,欢迎各位来star~

参考:

OpenGL ES 案例11:分屏滤镜
反色百科

系列文章目录

体系化学习系列博文,请看音视频系统学习的浪漫马车之总目录

实践项目: 介绍一个自己刚出炉的安卓音视频播放录制开源项目

相关专栏:

C/C++基础与进阶之路

音视频理论基础系列专栏

音视频开发实战系列专栏

轻松入门OpenGL系列
一看就懂的OpenGL ES教程——图形渲染管线的那些事
一看就懂的OpenGL ES教程——再谈OpenGL工作机制
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(一)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(二)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(三)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(四)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(五)
一看就懂的OpenGL ES教程——缓冲对象优化程序(一)
一看就懂的OpenGL ES教程——缓冲对象优化程序(二)
一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)
一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(实践篇)
一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年