本文正在参加「金石计划」
在上一篇文章 Opengl ES之LUT滤镜(上) 中我们详细介绍了基于图片纹理的2D LUT滤镜的使用,
这种方式也是目前大部分SDK在移动端的实现方式,既然有2D LUT滤镜那就有3D LUT,今天我们就来介绍下3D LUT的使用。
博主做这个demo之前查了一些资料,发现针对Opengl ES的3D LUT滤镜的资料比较稀缺,废了不少时间找到一个试了以下在某些机型上能生效,但是在部分手机上是有兼容性问题的,在iOS上是不生效的,
这里笔者也不敢打保票说我的demo就一定能兼容所有机型,但是至少实测起来是比笔者找到的那个例子兼容性好那么一丢丢…
所以这篇应该是需要付费的,也就6块9而已,半杯奶茶钱都不够…
同时在上一篇文章 Opengl ES之LUT滤镜(上) 我们留下一个问题,就是如何给LUT增加强度调节呢?
这个问题的答案也会在今天的demo中有所体现。
3D LUT
所谓的3D LUT就是将RGB分别作为与三维的XYZ建立坐标关系,原始图像的RGB颜色通过3D LUT映射后产生新的颜色数据 R1 G1 B1。
R1 = LUT(R)
G1 = LUT(G)
B1 = LUT(B)
映射关系如图所示:
通过这张图我们可以看出RGB与XYZ是对应的,个人认为其实3D LUT才是真正的LUT,2D的LUT只是为了减少内存使用,提升性能所做的一个简化。因此2D LUT相对与3D LUT来说
是会有一些细节点的损失的,如果想要展示更加精准细腻的颜色映射关系,那还是得使用3D LUT,这大概就是为了PC桌面端的视频剪辑软件多使用点cube文件作为3D LUT滤镜的一个原因吧。
总的来说就是3D LUT比2D LUT映射的细节点更多,更细腻,这就有点像HDR与SDR的关系。
在Opengl ES中使用3D LUT
首先我们看看点cube文件的内部结构,以下是我截取的部分点cube文件的内容:
#Created by: Adobe Photoshop Export Color Lookup Plugin
TITLE "1241667404168_.pic.jpg"
#LUT size
LUT_3D_SIZE 16
#data domain
DOMAIN_MIN 0.0 0.0 0.0
DOMAIN_MAX 1.0 1.0 1.0
#LUT data points
0.404297 0.404297 0.404297
0.414429 0.414429 0.414429
0.414429 0.414429 0.414429
0.415314 0.415314 0.415314
0.415314 0.415314 0.415314
0.410065 0.410065 0.410065
0.402374 0.402374 0.402374
一般来说带#好开头的是注释,我们读取的时候可以忽略,但是也并不是说每个点cube文件内部结构都是这样统一的,有些是是没有
#data domain
DOMAIN_MIN 0.0 0.0 0.0
DOMAIN_MAX 1.0 1.0 1.0
这些描述的,而有些又是带有TITLE
字段的描述,比如以下这个点cube文件的截取:
#Created by: Adobe Photoshop Export Color Lookup Plugin
TITLE "1241667404168_.pic.jpg"
#LUT size
LUT_3D_SIZE 16
#data domain
DOMAIN_MIN 0.0 0.0 0.0
DOMAIN_MAX 1.0 1.0 1.0
#LUT data points
0.404297 0.404297 0.404297
0.414429 0.414429 0.414429
0.414429 0.414429 0.414429
0.415314 0.415314 0.415314
0.415314 0.415314 0.415314
其实针对点cube文件,我们真正关心的数据点就两部分,一个以LUT_3D_SIZE
开头的那一行,另外以部分就是以数字开头的并且是以空格分割的数字行数据。
因此在Opengl ES中读取点cube文件作为3D纹理的步骤大概就是两步:
1、读取以LUT_3D_SIZE开头的行,使用空格分割,获取到后面的数字大小,然后根据这个大小给RGB纹理分配空间;
2、分配大小后读取以数字开头的行,使用空格蜂成一个大小为3的数组,这个数组代表的就是当前RGB的映射关系。
以下是笔者读取点cube文件的具体实例代码:
public class LUTCubeActivity extends BaseGlActivity{
private static final int FLOAT_SZ = Float.SIZE / 8;
@Override
public BaseOpengl createOpengl() {
LutCubeOpengl lutCubeOpengl = new LutCubeOpengl();
return lutCubeOpengl;
}
@Override
public Bitmap requestBitmap() {
BitmapFactory.Options options = new BitmapFactory.Options();
// 不缩放
options.inScaled = false;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_beauty,options);
return bitmap;
}
@Override
public CubeInfo requestCubeLut() {
CubeInfo cubeInfo = null;
BufferedReader bufReader = null;
FloatBuffer lutBuffer = null;
int size = 0;
try {
InputStreamReader inputReader = new InputStreamReader(getResources().getAssets().open("3dlut_01.cube"));
bufReader = new BufferedReader(inputReader);
String line = "";
while ((line = bufReader.readLine()) != null){
if(line.startsWith("LUT_3D_SIZE")){
size = Integer.valueOf(line.split(" ")[1]);
int allSize = size * size * size * 3;
//将位图加载到opengl中,并复制到当前绑定的纹理对象上
lutBuffer = ByteBuffer.allocateDirect(allSize * FLOAT_SZ)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
cubeInfo = new CubeInfo();
cubeInfo.setWidth(size);
cubeInfo.setHeight(size);
cubeInfo.setDepth(size);
cubeInfo.setBuffer(lutBuffer);
}else if(line.startsWith("0") || line.startsWith("1") || line.startsWith("2") || line.startsWith("3") || line.startsWith("4") || line.startsWith("5")
|| line.startsWith("6") || line.startsWith("7") || line.startsWith("8") || line.startsWith("9")){
String[] datas = line.split(" ");
for (int i = 0; i < datas.length; i++) {
lutBuffer.put(Float.valueOf(datas[i]));
}
}
}
} catch (IOException e) {
e.printStackTrace();
try {
bufReader.close();
} catch (IOException ex) {
ex.printStackTrace();
}
return null;
}
if(null != lutBuffer){
lutBuffer.position(0);
}
return cubeInfo;
}
}
CubeInfo信息代码:
public class CubeInfo {
private int width;
private int height;
private int depth;
private FloatBuffer buffer;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getDepth() {
return depth;
}
public void setDepth(int depth) {
this.depth = depth;
}
public FloatBuffer getBuffer() {
return buffer;
}
public void setBuffer(FloatBuffer buffer) {
this.buffer = buffer;
}
}
读取到cube内容之后,我们将cube数据作为3D LUT纹理的数据,使用函数glTexImage3D
进行纹理绑定即可使用了。
完整的cpp代码如下:
#include "LutCubeOpengl.h"
#include "../utils/Log.h"
#include <fstream>
// 顶点着色器
static const char *ver = "#version 300 es\n"
"in vec4 aPosition;\n"
"in vec2 aTexCoord;\n"
"out vec2 TexCoord;\n"
"void main() {\n"
" TexCoord = aTexCoord;\n"
" gl_Position = aPosition;\n"
"}";
// 片元着色器
static const char *fragment = "#version 300 es\n"
"precision mediump float;\n"
"precision mediump sampler3D;\n"
"in vec2 TexCoord;\n"
"uniform sampler2D ourTexture;\n"
"uniform sampler3D textureLUT;\n"
"uniform float filterRatio;\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" vec4 color = texture(ourTexture, TexCoord);\n"
" vec4 lutColor = texture(textureLUT, color.rgb);\n"
" FragColor = mix(color, vec4(lutColor.rgb, lutColor.w), filterRatio);\n"
"}";
const static GLfloat VERTICES_AND_TEXTURE[] = {
0.5f, -0.5f, // 右下
// 纹理坐标
1.0f, 1.0f,
0.5f, 0.5f, // 右上
// 纹理坐标
1.0f, 0.0f,
-0.5f, -0.5f, // 左下
// 纹理坐标
0.0f, 1.0f,
-0.5f, 0.5f, // 左上
// 纹理坐标
0.0f, 0.0f
};
// 使用byte类型比使用short或者int类型节约内存
const static uint8_t indices[] = {
// 注意索引从0开始!
// 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
// 这样可以由下标代表顶点组合成矩形
0, 1, 2, // 第一个三角形
1, 2, 3 // 第二个三角形
};
LutCubeOpengl::LutCubeOpengl() {
initGlProgram(ver, fragment);
positionHandle = glGetAttribLocation(program, "aPosition");
textureHandle = glGetAttribLocation(program, "aTexCoord");
textureSampler = glGetUniformLocation(program, "ourTexture");
lut_textureSampler = glGetUniformLocation(program, "textureLUT");
filterRatioHandle = glGetUniformLocation(program, "filterRatio");
LOGD("program:%d", program);
LOGD("positionHandle:%d", positionHandle);
LOGD("textureHandle:%d", textureHandle);
LOGD("textureSample:%d", textureSampler);
LOGD("filterRatioHandle:%d", filterRatioHandle);
// VAO
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// vbo
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(VERTICES_AND_TEXTURE), VERTICES_AND_TEXTURE,
GL_STATIC_DRAW);
// stride 步长 每个顶点坐标之间相隔4个数据点,数据类型是float
glVertexAttribPointer(positionHandle, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *) 0);
// 启用顶点数据
glEnableVertexAttribArray(positionHandle);
// stride 步长 每个颜色坐标之间相隔4个数据点,数据类型是float,颜色坐标索引从2开始
glVertexAttribPointer(textureHandle, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
(void *) (2 * sizeof(float)));
// 启用纹理坐标数组
glEnableVertexAttribArray(textureHandle);
// EBO
glGenBuffers(1, &ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 这个顺序不能乱啊,先解除vao,再解除其他的,不然在绘制的时候可能会不起作用,需要重新glBindBuffer才生效
// vao解除
glBindVertexArray(0);
// 解除绑定
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 解除绑定
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
LutCubeOpengl::~LutCubeOpengl() noexcept {
glDeleteBuffers(1, &ebo);
glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);
// ... 删除其他,例如fbo等
}
void LutCubeOpengl::setPixel(void *data, int width, int height, int length) {
imageWidth = width;
imageHeight = height;
glGenTextures(1, &imageTextureId);
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, imageTextureId);
// 为当前绑定的纹理对象设置环绕、过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
// 生成mip贴图
glGenerateMipmap(GL_TEXTURE_2D);
// 解绑定
glBindTexture(GL_TEXTURE_2D, 0);
}
void LutCubeOpengl::setLutCubeData(int width, int height, int depth, float *data) {
LOGD("setLutCubeData----width:%d,height:%d,depth:%d",width,height,depth)
glGenTextures(1, &lut_imageTextureId);
// 绑定纹理
glBindTexture(GL_TEXTURE_3D, lut_imageTextureId);
// 为当前绑定的纹理对象设置环绕、过滤方式
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage3D(GL_TEXTURE_3D, 0, GL_RGB16F, width, height, depth, 0, GL_RGB, GL_FLOAT, data);
// 解绑定
glBindTexture(GL_TEXTURE_3D, 0);
}
void LutCubeOpengl::onDraw() {
// 恢复绘制屏幕宽高
glViewport(0, 0, eglHelper->viewWidth, eglHelper->viewHeight);
// 绘制到屏幕
// 清屏
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(program);
// 激活纹理
glActiveTexture(GL_TEXTURE1);
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, imageTextureId);
glUniform1i(textureSampler, 1);
checkError(program);
// 设置强度,0到1
// 可设置成0到1对比效果
glUniform1f(filterRatioHandle,1);
// 激活纹理 lut
glActiveTexture(GL_TEXTURE2);
// 绑定纹理
glBindTexture(GL_TEXTURE_3D, lut_imageTextureId);
glUniform1i(lut_textureSampler, 2);
checkError(program);
// VBO与VAO配合绘制
// 使用vao
glBindVertexArray(vao);
// 使用EBO
// 使用byte类型节省内存
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, (void *) 0);
glUseProgram(0);
// vao解除绑定
glBindVertexArray(0);
// 禁用顶点
glDisableVertexAttribArray(positionHandle);
if (nullptr != eglHelper) {
eglHelper->swapBuffers();
}
glBindTexture(GL_TEXTURE_2D, 0);
glBindTexture(GL_TEXTURE_3D, 0);
}
下面简单介绍下上面代码的核心思想:
- setLutCubeData
这个方法就是初始化绑定3D LUT纹理,它的width
, height
,depth
三个参数理论上应该是相同的,就是上面点cube文件中以LUT_3D_SIZE
开头行的后面那个数字。
而纹理数据*data
则是点cube文件以数字开头的行,并且使用空格分割后的大小为3的数组的数据结合,在java层我们可以通过FloatBuffer的方式传递到JNI层使用,下面是核心代码。
// java层定义的native层方法
private native void n_setCubeLut(long ptr, int width, int height, int depth, FloatBuffer buffer);
// JNI中动态注册的方法
void gl_setCubeLut(JNIEnv *env, jobject thiz, jlong ptr,
jint width, jint height, jint depth,
jobject buffer){
BaseOpengl *baseOpengl = reinterpret_cast<BaseOpengl *>(ptr);
float *data = static_cast<float *>(env->GetDirectBufferAddress(buffer));
// 获取buffer的容量
jlong bufferCapacity = env->GetDirectBufferCapacity(buffer);
baseOpengl->setLutCubeData(width,height,depth,data);
}
- 片元着色器中filterRatio的含义
这个就是滤镜强度的参数,现在程序中固定写为了1,开发者可以在0到1之间测试下具体的强度效果,mix作为内置函数,主要就是混合功能,之前我们在水印贴图的例子中也使用过这个函数。
demo运行结果图
以下这张是需要增加滤镜的原图:
所用到的LUT滤镜是程序源码中的src/main/assets/3dlut_01.cube
以下这张是运行结果图:
系列教程源码
Opengl ES系列入门介绍
Opengl ES之EGL环境搭建
Opengl ES之着色器
Opengl ES之三角形绘制
Opengl ES之四边形绘制
Opengl ES之纹理贴图
Opengl ES之VBO和VAO
Opengl ES之EBO
Opengl ES之FBO
Opengl ES之PBO
Opengl ES之YUV数据渲染
YUV转RGB的一些理论知识
Opengl ES之RGB转NV21
Opengl ES之踩坑记
Opengl ES之矩阵变换(上)
Opengl ES之矩阵变换(下)
Opengl ES之水印贴图
Opengl ES之纹理数组
OpenGL ES之多目标渲染(MRT
Opengl ES之LUT滤镜(上)
关注我,一起进步,人生不止coding!!!