音视频开发:OpenGL + OpenGL ES + Metal 系列文章汇总
通过OpenGL ES实现多种滤镜效果,包括分屏滤镜、灰度滤镜、颠倒滤镜、马赛克滤镜、动效滤镜(6种),以此来简单认识常用滤镜,并借此提升着色器的实现算法能力。
主要内容:
- 分屏滤镜
- 灰度滤镜
- 颠倒滤镜
- 马赛克滤镜
- 动效滤镜
1、滤镜认识
通过第七、八、九章的博客的阅读,对OpenGL ES的基本实现流程有了清晰的认识。
但是在实际工作中我们可能会有一些滤镜实现,因此本文就进行滤镜的实现的简单使用。虽然现在成熟的滤镜已经有很多了,而且复杂的滤镜效果也有专门团队负责,但是通过基础滤镜的实现可以对OpenGL ES的实现有深刻的理解,对着色器的使用也更加熟悉。
在本案例中我会详细的解读着色器的实现,虽然这些效果都已经有成熟的算法,但这也是一个算法提升的过程。
2、分屏滤镜
本案例的目的是理解如何用GLSL实现分屏(2/3/4/6/9)滤镜
注:因时间仓促,一些图直接取自Style_月月的博客OpenGL ES 案例11:分屏滤镜,侵删。
案例地址: 滤镜处理-分屏滤镜
效果:
2.1 简单介绍
分屏滤镜就是将一张纹理以不同的样式分成几等份显示出来
实现功能:
- 实现一分屏效果
- 实现二分屏效果
- 实现三分屏效果
- 实现四分屏效果
- 实现六分屏效果
- 实现九分屏效果
实现思路:
- 一张图片的正常加载
- 图片的纹理映射修改
重点在于着色器对纹理进行不同方式的映射,其他的图片的加载流程基本上与单张图片的实现一样,只是增加了不同界面的切换
因此可以分成三种情况考虑:
- GLSL实现图片的加载
- 着色器对分屏效果的处理
- 不同界面切换时着色器的切换过程
GLSL实现图片的加载详情可以查看OpenGL ES - GLSL的使用的图片加载案例,这里就不再赘述了。
2.2 着色器对分屏效果的处理
因为仅仅涉及纹理的映射,因此顶点着色器不发生修改,只修改片元着色器
顶点着色器
做两件事:
- 获取到顶点数据
- 传递纹理坐标到片元着色器
attribute vec4 Position;
attribute vec2 TextureCoords;
varying vec2 TextureCoordsVarying;
void main (void) {
gl_Position = Position;
TextureCoordsVarying = TextureCoords;
}
2.2.1 一分屏
代码:
precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
void main (void) {
vec4 mask = texture2D(Texture, TextureCoordsVarying);
gl_FragColor = vec4(mask.rgb, 1.0);
}
实现:
- 获取到每个顶点的纹素
- 将颜色设置透明度返回
2.2.2 - 二分屏
示意图:
原理分析:
- 取出0.25到0.75的部分内容显示到上部和下部
- 可以看出将中部向上平移0.25即形成上部图片。
- 将中部向下平移0.25即形成下部图片。
实现思路:
- 判断坐标在上部,就需要+0.25
- 判断坐标在下部,就需要-0.25。
代码:
precision highp float;
uniform sampler2D Texture;
varying highp vec2 TextureCoordsVarying;
void main() {
vec2 uv = TextureCoordsVarying.xy;
float y;
if (uv.y >= 0.0 && uv.y <= 0.5) {
y = uv.y + 0.25;
} else {
y = uv.y - 0.25;
}
gl_FragColor = texture2D(Texture, vec2(uv.x, y));
}
2.2.3 - 三分屏
示意图:
原理分析:
- 1/3的图的纹理坐标为(0,1/3),需要显示的图所在范围的纹理坐标为(1/3,2/3),因此需要将坐标都+ 1/3。
- 很明显中间的纹理不变
- 下边的图片的纹理坐标为(2/3,3/3),需要显示的图所在范围的纹理坐标为(1/3,2/3)。因此需要将坐标都- 1/3。
实现思路:
- 判断纹理坐标为(0,1/3),坐标+ 1/3.
- 判断纹理坐标为(1/3,2/3),不做修改
- 判断纹理坐标为(2/3,3/3),坐标- 1/3.
代码:
precision highp float;
uniform sampler2D Texture;
varying highp vec2 TextureCoordsVarying;
void main() {
vec2 uv = TextureCoordsVarying.xy;
if (uv.y < 1.0/3.0) {
uv.y = uv.y + 1.0/3.0;
} else if (uv.y > 2.0/3.0){
uv.y = uv.y - 1.0/3.0;
}
gl_FragColor = texture2D(Texture, uv);
}
2.2.4 - 四分屏
示意图:
原理分析:
- 很明显是将之前的一整块图片映射到小的四个位置
- 在x轴上,上半部分从原来的(0,0.5)变成了(0,1),所以需要坐标*2,
- 在x轴上,下半部分从原来的(0.5,1.0)变成了(0,1),依靠上面的计算可以得到应该是(x-0.5)*2
- 在y轴同样,上半部分从原来的(0,0.5)变成了(0,1),所以需要坐标*2
- 在y轴,下半部分从原来的(0.5,1.0)变成了(0,1),依靠上面的计算可以得到应该是(y-0.5)*2
实现思路:
- 分别判断x/y轴,是否大于0.5
- 如果小于0.5,直接*2
- 如果大于0.5,(n-0.5)*2
代码:
precision highp float;
uniform sampler2D Texture;
varying highp vec2 TextureCoordsVarying;
void main() {
vec2 uv = TextureCoordsVarying.xy;
if(uv.x <= 0.5){
uv.x = uv.x * 2.0;
}else{
uv.x = (uv.x - 0.5) * 2.0;
}
if (uv.y<= 0.5) {
uv.y = uv.y * 2.0;
}else{
uv.y = (uv.y - 0.5) * 2.0;
}
gl_FragColor = texture2D(Texture, uv);
}
2.2.5 - 六分屏
示意图:
原理分析:
- 明显看出是结合了二分屏和三分屏的实现,只不过是分别用x轴用三分屏原理,y轴用二分屏原理。所以这里不再分析。
代码:
precision highp float;
uniform sampler2D Texture;
varying highp vec2 TextureCoordsVarying;
void main() {
vec2 uv = TextureCoordsVarying.xy;
if(uv.x <= 1.0 / 3.0){
uv.x = uv.x + 1.0/3.0;
}else if(uv.x >= 2.0/3.0){
uv.x = uv.x - 1.0/3.0;
}
if(uv.y <= 0.5){
uv.y = uv.y + 0.25;
}else {
uv.y = uv.y - 0.25;
}
gl_FragColor = texture2D(Texture, uv);
}
2.2.6 - 九分屏
示意图:
原理分析:
原理与四分屏一样。一看便知,不再赘言了。
代码:
precision highp float;
uniform sampler2D Texture;
varying highp vec2 TextureCoordsVarying;
void main() {
vec2 uv = TextureCoordsVarying.xy;
if (uv.x < 1.0 / 3.0) {
uv.x = uv.x * 3.0;
} else if (uv.x < 2.0 / 3.0) {
uv.x = (uv.x - 1.0 / 3.0) * 3.0;
} else {
uv.x = (uv.x - 2.0 / 3.0) * 3.0;
}
if (uv.y <= 1.0 / 3.0) {
uv.y = uv.y * 3.0;
} else if (uv.y < 2.0 / 3.0) {
uv.y = (uv.y - 1.0 / 3.0) * 3.0;
} else {
uv.y = (uv.y - 2.0 / 3.0) * 3.0;
}
gl_FragColor = texture2D(Texture, uv);
}
2.3 着色器的切换
- 点击不同的选项,开始对不同的着色器进行重新编译、链接
- 重新绘制
代码:
- (void)filterBar:(FilterBar *)filterBar didScrollToIndex:(NSUInteger)index {
//1. 选择默认shader
if (index == 0) {
[self setupNormalShaderProgram];
}else if(index == 1)
{
[self setupSplitScreen_2ShaderProgram];
}else if(index == 2)
{
[self setupSplitScreen_3ShaderProgram];
}else if(index == 3)
{
[self setupSplitScreen_4ShaderProgram];
}else if(index == 4)
{
[self setupSplitScreen_6ShaderProgram];
}else if(index == 5)
{
[self setupSplitScreen_9ShaderProgram];
}
// 重新开始滤镜动画
[self startFilerAnimation];
}
3、灰度滤镜
将RGB的色值分别进行灰度计算,得到的RGB颜色就是灰度颜色
具体实现:
- 定义一个计算的灰度值,之后让这个灰度值与我们采样的色值进行点乘计算就得到最终的颜色值
- 灰度值是向量、采样色值也是向量,计算后得到的是标量
- 之后再将这个标量通过vec3()得到向量(注意此时RGB都是这个标量值)
常见灰度值计算方式
- 权值法:处理后的图片比较逼真
- 浮点算法: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);
}
4、颠倒滤镜
对纹理坐标的y坐标进行颠倒计算就可以实现颠倒效果,
代码:
precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
void main(){
vec4 mask = texture2D(Texture, vec2(TextureCoordsVarying.x, 1.0-TextureCoordsVarying.y));
gl_FragColor = vec4(mask.rgb, 1.0);
}
注意:
- 颠倒滤镜是对纹理坐标的处理,而不是对顶点坐标的处理
- 纹理坐标的处理可以在顶点着色器也可以在片元着色器
5、马赛克滤镜
马赛克效果就是把图片的一个相当大小的区域用同一个点的颜色来表示,可以认为是对规模的降低图像的分辨率,从而让图像的一些细节隐藏起来。主要任务是计算任意一个纹理坐标需要采用哪个纹理坐标的色值来显示。重点在于数学计算
下面分析三种马赛克滤镜:正方形马赛克、六边形马赛克、三角形马赛克。
滤镜效果:
5.1 正方形马赛克
将纹理划分成一个一个小正方形,每个正方形选取左上角坐标的纹素进行上色,因此重点在于获取当前坐标所在小正方形的左上角纹理坐标。
5.1.1 原理分析
- 将整个图片划分成一块一块的,某一块区域显示为其中一种色值
- 这一块区域的大小就是马赛克的大小
- 这一种色值为这块区域的最左上角的像素的颜色
5.1.2 实现思路
- 先决定马赛克的大小
- 将当前纹理坐标先除马赛克大小并向下取整再乘以马赛克大小,这样就取到左上角的颜色(通过向下取整得到左上角位置)
5.1.3 具体代码
precision mediump float;
varying vec2 TextureCoordsVarying;
uniform sampler2D Texture;
//小马赛克大小
const vec2 MosoicSize = vec2(0.1,0.1);
void main()
{
//floor(x) 内建函数,返回小于/等于x最大的整数,即向下取整
//向下取整后再乘以小马赛克大小就得到马赛克最左上角的纹理坐标
vec2 XYMosaic = vec2(floor(TextureCoordsVarying.x/MosoicSize.x)*MosoicSize.x, floor(TextureCoordsVarying.y/MosoicSize.y)*MosoicSize.y);
vec4 color = texture2D(Texture, XYMosaic);
gl_FragColor = color;
}
5.2 六边形马赛克
将纹理图形划分出一个个的六边形,选取六边形的中心点颜色值填充整个六边形的颜色
5.2.1 原理分析
- 将一张纹理分割成多个六边形,再将六边形的中心点连接起来形成矩形
- 根据矩形的奇偶排列情况求出对应的2个中心点
- 获取到图片的每个像素,需要判断每个像素离哪个中心点更近(也就是矩形的某个顶点),将其渲染成该中心点的颜色
5.2.2 实现思路
这里就有几个问题需要解决
1、通过纹理坐标拿到矩形坐标(先获取左上角第一个坐标,剩下三个坐标可以推算出来)
2、矩形的哪两个顶点是中心点
3、像素离哪个顶点最近
实现:
5.2.2.1 通过纹理坐标拿到矩形左上角坐标
这里和正方形马赛克的计算方式一样,也是通过向下取整获取到像素点所在矩形的左上角纹理坐标,不一样的是矩形需要知道宽高比
矩阵宽高比:
长比宽用TB表示,为3:√3,相当于将一个纹理等分成多个矩形单元。每个矩形的长宽比为3:√3
计算出矩形坐标
- 假设一个六边形的边长为length,则矩形的长宽为(3* length,√3*length)
- 那么一个纹理坐标的(x,y)所对应的矩形坐标(wx,wy)为((int(x/(3* length)),(int(x/√3*length))
- 这里得到的是int型,因此是向下取整了,小数点后数字被删掉
- 因此获取的是矩形左上角顶点
- 上面已经向下取整,因此这里再将wx和wy分别* length * 3就得到了矩形左上角的纹理坐标
- 再经过推算就可以得到另外三个顶点的坐标
5.2.2.2 拿到矩形属于中心点的纹理坐标
说明:
- 矩形不是所有的点都是中心点
- 一个矩形只有两个是中心点,首先要判断哪些点是中心点
- 所有的矩阵样式只有这两种类型
- 一种是左上角和右下角是中心点
- 一种是左下角和右上角是中心点
判断方式:
- 偶行偶列:左上右下
- 偶行奇列:左下右上
- 奇行偶列:左下右上
- 奇行奇列:左上右下
两种矩形的计算方式
5.2.2.3 判断像素处在哪个矩形
说明:
- 分别计算像素距离每个矩形中心点的距离,
- 之后比较结果最小的那个中心点的像素颜色就可以填充到这个像素上
5.2.3 具体代码
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)
//在内部再乘以(length * TB)就得到了顶点的纹理坐标
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;
}
5.3 三角形马赛克
三角形是在六边形的基础上实现的,得到三角形的前提,就是的先有六边形,然后将正六边形6等分,每个三角形都是正三角形,每个正三角形都以自身的中心点的颜色填充整个三角形的颜色
5.3.1 需要解决的问题
1. 求出纹理坐标处在哪个三角形
通过纹理坐标与六边形中心点的夹角来判断处在哪个三角形
2. 求出三角形的中心点
数学上计算六边形的每个三角形的中心点
5.3.2 具体代码
//获取像素点与中心点的角度
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;
atan是GLSL中的内建函数,有两种计算方式
1、atan(y,x) 值域是[0,π],
2、atan(y/x),值域是[-π/2, π/2]
5.4 总结
- 正方形马赛克是通过获取每个小马赛克的左上角纹理坐标的纹素来填充该马赛克颜色的
- 具体通过先除以小马赛克大小之后向下取整得到处在该小马赛克所有坐标都取左上角的颜色
- 再乘以小马赛克大小就得到真实的纹理坐标
- 六边形马赛克是将每个六边形的中心点填充整个六边形来实现的
- 通过将六边形划分成矩形,按照正方形马赛克那样也向下取整得到矩形左上角坐标,进而得到每个矩形的坐标
- 再通过左上角坐标的奇偶性来判断矩形的哪两个顶点是中心点
- 纹理坐标与中心点进行比较,拿到较小者作为该纹理坐标的颜色
- 三角形马赛克是在六边形马赛克的基础上进行六等分,每个三角形取自身的中心点的颜色进行填充
- 纹理坐标可以通过其与六边形的夹角来判断处在哪个三角形
- 之后拿到这个三角形的中心点来返回颜色
- 至于如何计算三角形的中心点这是初中数学,比较简单