源码:https://github.com/smzhldr/AGLFramework
一、前言
学习OpenGL,一定要学习Lookup滤镜,不光是因为其功能强大,还因为lookup滤镜涵盖OpenGL ES初级阶段的知识点比较全面,只要把lookup滤镜搞懂了,说明基础已经差不多了,也为后面进一步提高铺平了道路。在美颜相机类产品包括视频直播类项目诸如抖音、快手、唱吧、陌陌、Faceu激萌等都能见到诸如“柔和”,“温暖”,“复古”,“清新”,“黑白”…功能丰富的滤镜,其实都是用了lookup滤镜。看起来比较神奇,我们就一块揭开她的神秘面纱。
二、先看一张lookup滤镜的效果

左边是一张原图,右边是一张使用了某一种特效的lookup滤镜处理后的图片,看起来真有一中恋爱的感觉。
三、Lookup滤镜原理分析
1. lookup table颜色替换原理
lookup table(颜色查找表),简而言之就是通过将每一个原始的颜色进行转换之后成为一个新的颜色。打一个比方,比如原始颜色是红色(r:255,g:0,b:0),进行转换后变为绿色(r:0,g:255,b:0),以后所有是红色的地方都会被自动转换为绿色。
我们知道在RGB888色彩每个分量的有效值是[0,256],那么完整采样的色域空间为就256256256约为16M,意味着理论上约有16M种颜色可供调整。显然实际上并不会用到那么多的颜色,通常会通过列举节点来储存,而两个节点之间的颜色通过插值运算出来。
大多数情况下颜色查找表是由如下的图片生成的:
上图是一张原始图像的RGB颜色查找表,是一个二维图像,但RGB是一个三维元素,这其中有一个映射关系,将二维图像转换成三维三色表用来查询,首先看一下三维颜色模型,如下图:
上图中X,Y,Z三个方向分别和R,G,B色值对应
若果垂直于Z轴将这个颜色立方体切割好多次,每次切割的结果将如下图:
上面说了实际是用插值算法来算的,所以并不用切割无数次以表示精确,切割(2n)^2次确保可以组成一个边长偶数的正方形就可以了,一半来说切割64次组成一个8x8的正方形或者切割16次组成4x4的正方形,这中间设计性能和效果的折中。组合的打正方形就如文章最前面展示的8x8的原始颜色查找表一样,每个小正方形的x轴表示R分量,y轴表示G分享,B分量是通过小正方形在大正方形中的位置索引来算出来的,对应的也有64种,那么我们的三维颜色查找表就构建好了,只需要将原始图像的每一个像素的颜色值替换为颜色查找表中的值就可以了。
2. 颜色查找替换算法分析
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
uniform sampler2D lookupTexture; // lookup texture
uniform highp float lookupDimension; // 64
uniform lowp float intensity;
void main()
{
highp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
//B通道上对应的数值
highp float blueColor = textureColor.b * (lookupDimension - 1.0);
highp float lookupDimensionSqrt = sqrt(lookupDimension);
// 计算临近两个B通道所在的方形LUT单元格(从左到右从,上到下排列)
highp vec2 quad1;
quad1.y = floor(floor(blueColor) / lookupDimensionSqrt);
quad1.x = floor(blueColor) - (quad1.y * lookupDimensionSqrt);
highp vec2 quad2;
quad2.y = floor(ceil(blueColor) / lookupDimensionSqrt);
quad2.x = ceil(blueColor) - (quad2.y * lookupDimensionSqrt);
//在对应的小正方形中查找原始图像当前像素锁对应的查找表中的位置
//0.5是小正方形里的小正方形的位置取均值
highp vec2 texPos1;
texPos1.x = (quad1.x / lookupDimensionSqrt) + 0.5/lookupDimensionSqrt/lookupDimension + ((1.0/lookupDimensionSqrt - 1.0/lookupDimensionSqrt/lookupDimension) * textureColor.r);
texPos1.y = (quad1.y / lookupDimensionSqrt) + 0.5/lookupDimensionSqrt/lookupDimension + ((1.0/lookupDimensionSqrt - 1.0/lookupDimensionSqrt/lookupDimension) * textureColor.g);
highp vec2 texPos2;
texPos2.x = (quad2.x / lookupDimensionSqrt) + 0.5/lookupDimensionSqrt/lookupDimension + ((1.0/lookupDimensionSqrt - 1.0/lookupDimensionSqrt/lookupDimension) * textureColor.r);
texPos2.y = (quad2.y / lookupDimensionSqrt) + 0.5/lookupDimensionSqrt/lookupDimension + ((1.0/lookupDimensionSqrt - 1.0/lookupDimensionSqrt/lookupDimension) * textureColor.g);
//根据当前像素查找到的相邻的两个小正方形的位置从纹理中取出形影的像素颜色值
lowp vec4 newColor1 = texture2D(lookupTexture, texPos1);
lowp vec4 newColor2 = texture2D(lookupTexture, texPos2);
//根据蓝色通道的值对生成的相邻的两个新图像色值做加权
lowp vec4 newColor = vec4(mix(newColor1.rgb, newColor2.rgb, fract(blueColor)), textureColor.w);
//根据intensity控制效果程度
gl_FragColor = mix(textureColor, newColor, intensity);
}
对着上面shader里的注释和上面讲到的原理,相信很容易就能看懂其中的道理了。
三、LookupFilter实现
public class LookupFilter extends AGLFilter {
private int glUniIntensity;
private int glUniLookupTexture;
private float intensity = 1f;
private Bitmap bitmap;
private volatile boolean isNewFilter;
public LookupFilter(Context context) {
super(context, R.raw.lookup_f);
}
@Override
protected void onInit() {
glUniIntensity = GLES20.glGetUniformLocation(programId, "intensity");
glUniLookupTexture = GLES20.glGetUniformLocation(programId, "lookupTexture");
}
@Override
protected void onDrawArraysPre(Frame frame) {
GLES20.glUniform1f(glUniIntensity, intensity);
if (isNewFilter && bitmap != null) {
int lookupTexture = OpenGlUtils.loadTexture(bitmap, OpenGlUtils.NO_TEXTURE);
GLES20.glActiveTexture(GLES20.GL_TEXTURE4);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, lookupTexture);
GLES20.glUniform1i(glUniLookupTexture, 4);
isNewFilter = false;
}
}
public void setIntensity(float intensity) {
this.intensity = intensity;
}
public void setBitmap(@NonNull Bitmap bitmap) {
if (!isNewFilter) {
this.bitmap = bitmap;
isNewFilter = true;
}
}
}
代码不过短短的几十行,却能实现如此神奇的效果,还能调节所需的颜色的比例。此Filter是基于前面章节封装号的框架写的,有了前面的章节的内容,此Filter的内容理解起来应该非常容易。
源码 里有我从网上收集的几十种颜色查找表的图片,利用现有的资源很容易做出小清新的滤镜