之前写过的文章(使用OpenGL实现滤镜转换的一种思路_轮子初级玩家-CSDN博客),我把一整个Lut滤镜图作为单个纹理贴图,把图像原颜色采样后当作坐标,然后从lut纹理中查找出替换颜色实现滤镜功能,这是最简易的一种滤镜实现方式,但由于lut纹理的T轴占比过长,计算进度上容易导致命中错误的滤镜页的问题,那是否有办法实现更高精度的滤镜呢?其实是有的,而且我优化了一下。
本文默认读者已经具备基本的OpenGL代码阅读能力,所以只打算写一些必要的代码和解析。
一、片元渲染器的编写:
首先我们明确需要两个纹理,一个为原图纹理sTexture,用于给OpenGL灌入原画面;一个是滤镜Lut表纹理lutTexture,类型分别为sampler2D和sampler2DArray。
看到sampler2DArray相信读者已经猜到降低进度要求的办法是什么了,就是通过使用2d纹理数组,把一整个lut表,通过纹理数组,分割为一页页,即可解决行和页的命中精度问题。
由于这是纹理数组,所以对比二维纹理ST坐标多了一维R,R可以理解为下标,每+1可下翻一个二维纹理。而原颜色的R分量可以理解为页码,G可以理解为行,B为列,那么颜色替换的完整代码就是:
#version 300 es
precision highp float;
precision highp sampler2DArray;
uniform sampler2D sTexture;//图像纹理输入
uniform sampler2DArray lutTexture;//滤镜纹理输入
uniform float pageSize;
in vec4 fragObjectColor;//接收vertShader处理后的颜色值给片元程序
in vec2 fragVTexCoord;//接收vertShader处理后的纹理内坐标给片元程序
out vec4 fragColor;//输出到的片元颜色
void main() {
vec4 srcColor = texture(sTexture, fragVTexCoord);
srcColor.r = clamp(srcColor.r, 0.01, 0.99);
srcColor.g = clamp(srcColor.g, 0.01, 0.99);
srcColor.b = clamp(srcColor.b, 0.01, 0.99);
fragColor = texture(lutTexture, vec3(srcColor.b, srcColor.g, srcColor.r * (pageSize - 1.0)));
}
为了防止采样越界,我使用了clamp把原颜色每一个分量都限制在(0.01, 0.99)范围中。
二、纹理载入代码:
很多时候,为了节约用户的存储空间消耗,而且LUT表的颜色单元之间存在一定的线性关系,LUT表并不是对RGB颜色的256种可能全部一一对应有替换种类的,例如我这个demo中使用的lut表,每种颜色分量只有64种可对应的颜色,所以最适合使用的纹理采样方式就是线性采样,顺带可以把颜色映射之间缺乏的中间颜色通过线性关系求出,如果使用NEAREST采样,虽然可以降低运算量,却无法做到平滑的线性映射,更像阶梯状的映射。所以还是使用线性映射更好,至于重复模式用最常见的截取式的就足够了。纹理数据的加载因为需要告诉OpenGL这一份数据的分页数——也就是数组数组的大小,也叫深度depth,所以需要使用glTexImage3D函数。
glGenTextures(1, mLutTexutresPointers);
glBindTexture(GL_TEXTURE_2D_ARRAY, mLutTexutresPointers[0]);
glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
int longLen = mLutWidth > mLutHeight ? mLutWidth : mLutHeight;
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, mLutUnitLen, mLutUnitLen, longLen / mLutUnitLen, 0, GL_RGBA, GL_UNSIGNED_BYTE, mTestPixels);
free(mTestPixels);
三、drawcall代码编写:
这个和平常的纹理绑定并draw到顶点数组构成的面上没啥不同,只是纹理多了一个:
glActiveTexture(GL_TEXTURE0); //激活0号纹理
glBindTexture(GL_TEXTURE_2D, mInputTexturesArrayPointer); //0号纹理绑定内容
glUniform1i(glGetUniformLocation(mImageProgram.programHandle, "sTexture"), 0); //映射到渲染脚本,获取纹理属性的指针
glActiveTexture(GL_TEXTURE1); //激活1号纹理
glBindTexture(GL_TEXTURE_2D_ARRAY, mLutTexutresPointers[0]);
glUniform1i(glGetUniformLocation(mImageProgram.programHandle, "lutTexture"), 1); //映射到渲染脚本,获取纹理属性的指针
glUniform1f(glGetUniformLocation(mImageProgram.programHandle, "pageSize"), longLen / mLutUnitLen);
实际效果:
这次使用的是一款叫“朦胧”的LUT滤镜,为了对比原纹理和使用lut滤镜表替换颜色后生成的纹理画面,我把shader做了一点修改,使得画面左边为原画面,右边为滤镜效果画面(我的Demo使用了多个shaderProgram搭配FBO以流水线的方式处理,滤镜shader为最后一步):
if (fragVTexCoord.x >= 0.5) {
fragColor = texture(lutTexture, vec3(srcColor.b, srcColor.g, srcColor.r * 63.0));
} else {
fragColor = srcColor;
}
我的滤镜、FBO多重渲染的DEMO
详细代码可以参考我的学习工程:
使用的滤镜Lut图像: