假如我希望实现如下特性:
1、使用片元shader,把YUV信号作为纹理输入,采样过程中转换为RGB信号。
2、把第1步取得的画面通过片元shader,使用3*3的卷积核,实现卷积模糊。
那么,就有如下几种方案:
第一种:
片元shader每次采样3*3个坐标,转换后记录到数组,之后对数组实现卷积处理,最后输出片元颜色。
第二种:
片元shader每次采样1个坐标,转换后直接输出到片元颜色,此时采样后的输出就会在指定的frameBuffer中。然后第二个片元shader使用framebuffer作为纹理输入,采样时每次采样3*3的坐标进行卷积。
两种方案的采样和计算工作量是一致的,但很显然第一种方案会随着希望插入的处理步骤的增加,而使得整个片元shader逐渐膨胀,而且也不方便灵活增删处理过程。假如我想做一个视频剪辑软件,处理步骤可以是1、2、3,也可以是先3、1、2,这样在单个shader就很难实现这么灵活的分配了,那是否有更好的办法呢?
参考了一下吴亚峰的《OpenGL ES 3.x游戏开发 下卷》中1.6章节《帧缓冲与渲染缓冲》与1.7章节《多重渲染目标》后,我发现了以下几个可能能用得上的OpenGL特性:
1、FBO:
2、FBO映射为纹理:
其实这两个特性也是所谓离屏渲染所依赖的特性。通过以上两个feature,那完全可以一个shader渲染到framebuffer中的图像,拿过来继续作为纹理输入进行下一步的加工处理。每一步的输出,都可以下一步加工的纹理输入,这样即可随意分配shader的处理顺序和深度,类似于可以自由装配的OpenGL图像处理流水线工厂,使得灵活性大大增加,能更容易实现多图层、多流水线,适合鱼视频编辑、多重图像处理等领域。我的架构设计如下:
每个Layer可以按照需要插入多个相同或者不同的shaderProgram,顺序随意,最后全部Layer一一叠加为最终画面。
实际代码如下:
首先是图层类 Layer.cpp:
//
// Created by jiezhuchen on 2021/7/5.
//
#include
#include
#include "Layer.h"
#include "RenderProgram.h"
#include "shaderUtil.h"
using namespace OPENGL_VIDEO_RENDERER;
Layer::Layer(float x, float y, float z, float w, float h, int windowW, int windowH) {
mX = x;
mY = y;
mZ = z;
mWidth = w;
mHeight = h;
mWindowW = windowW;
mWindowH = windowH;
mRenderSrcData.data = nullptr;
createFrameBuffer();
createLayerProgram();
}
Layer::~Layer() {
destroy();
}
void Layer::initObjMatrix() {
//创建单位矩阵
setIdentityM(mObjectMatrix, 0);
setIdentityM(mUserObjectMatrix, 0);
setIdentityM(mUserObjectRotateMatrix, 0);
}
void Layer::scale(float sx, float sy, float sz) {
scaleM(mObjectMatrix, 0, sx, sy, sz);
}
void Layer::translate(float dx, float dy, float dz) {
translateM(mObjectMatrix, 0, dx, dy, dz);
}
void Layer::rotate(int degree, float roundX, float roundY, float roundZ) {
rotateM(mObjectMatrix, 0, degree, roundX, roundY, roundZ);
}
/**用户直接设定缩放量**/
void Layer::setUserScale(float sx, float sy, float sz) {
mUserObjectMatrix[0] = sx;
mUserObjectMatrix[5] = sy;
mUserObjectMatrix[10] = sz;
}
/**用户直接设定位置偏移量**/
void Layer::setUserTransLate(float dx, float dy, float dz) { //由于opengl的矩阵旋转过,所以原本改3、7、11的矩阵位置变成12、13、14
mUserObjectMatrix[12] = dx;
mUserObjectMatrix[13] = dy;
mUserObjectMatrix[14] = dz;
}
/**用户直接设定旋转量**/
void Layer::setUserRotate(float degree, float vecX, float vecY, float vecZ) {
setIdentityM(mUserObjectRotateMatrix, 0); //先复原
rotateM(mUserObjectRotateMatrix, 0, degree, vecX, vecY, vecZ);
}
void Layer::locationTrans(float cameraMatrix[], float projMatrix[], int muMVPMatrixPointer) {
multiplyMM(mMVPMatrix, 0, mUserObjectMatrix, 0, mObjectMatrix, 0);
multiplyMM(mMVPMatrix, 0, mUserObjectRotateMatrix, 0, mMVPMatrix, 0);
multiplyMM(mMVPMatrix, 0, cameraMatrix, 0, mMVPMatrix, 0); //将摄像机矩阵乘以物体矩阵
multiplyMM(mMVPMatrix, 0, projMatrix, 0, mMVPMatrix, 0); //将投影矩阵乘以上一步的结果矩阵
glUniformMatrix4fv(muMVPMatrixPointer, 1, false, mMVPMatrix); //将最终变换关系传入渲染管线
}
//todo 每一次修改都会导致绑定的纹理本身被修改,这样会导致循环论证一样的问题,所以要使用双Framebuffer
void Layer::createFrameBuffer() {
int frameBufferCount = sizeof(mFrameBufferPointerArray) / sizeof(GLuint);
//生成framebuffer
glGenFramebuffers(frameBufferCount, mFrameBufferPointerArray);
//生成渲染缓冲buffer
glGenRenderbuffers(frameBufferCount, mRenderBufferPointerArray);
//生成framebuffer纹理pointer
glGenTextures(frameBufferCount, mFrameBufferTexturePointerArray);
//遍历framebuffer并初始化
for (int i = 0; i < frameBufferCount; i++) {
//绑定帧缓冲,遍历两个framebuffer分别初始化
glBindFramebuffer(GL_FRAMEBUFFER, mFrameBufferPointerArray[i]);
//绑定缓冲pointer
glBindRenderbuffer(GL_RENDERBUFFER, mRenderBufferPointerArray[i]);
//为渲染缓冲初始化存储,分配显存
glRenderbufferStorage(GL_RENDERBUFFER,
GL_DEPTH_COMPONENT16, mWindowW, mWindowH); //设置framebuffer的长宽
glBindTexture(GL_TEXTURE_2D, mFrameBufferTexturePointerArray[i]); //绑定纹理Pointer
glTexParameterf(GL_TEXTURE_2D,//设置MIN采样方式
GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D,//设置MAG采样方式
GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D,//设置S轴拉伸方式
GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D,//设置T轴拉伸方式
GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D//设置颜色附件纹理图的格式
(
GL_TEXTURE_2D,
0, //层次
GL_RGBA, //内部格式
mWindowW, //宽度
mWindowH, //高度
0, //边界宽度
GL_RGBA, //格式
GL_UNSIGNED_BYTE,//每个像素数据格式
nullptr
);
glFramebufferTexture2D //设置自定义帧缓冲的颜色缓冲附件
(
GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0, //颜色缓冲附件
GL_TEXTURE_2D,
mFrameBufferTexturePointerArray[i], //纹理id
0 //层次
);
glFramebufferRenderbuffer //设置自定义帧缓冲的深度缓冲附件
(
GL_FRAMEBUFFER,
GL_DEPTH_ATTACHMENT, //深度缓冲附件
GL_RENDERBUFFER, //渲染缓冲
mRenderBufferPointerArray[i] //渲染深度缓冲id
);
}
//绑回系统默认framebuffer,否则会显示不出东西
glBindFramebuffer(GL_FRAMEBUFFER, 0);//绑定帧缓冲id
}
void Layer::createLayerProgram() {
char vertShader[] = GL_SHADER_STRING(
##version 300 es\n
uniform mat4 uMVPMatrix; //旋转平移缩放 总变换矩阵。物体矩阵乘以它即可产生变换
in vec3 objectPosition; //物体位置向量,参与运算但不输出给片源
in vec4 objectColor; //物理颜色向量
in vec2 vTexCoord; //纹理内坐标
out vec4 fragObjectColor;//输出处理后的颜色值给片元程序
out vec2 fragVTexCoord;//输出处理后的纹理内坐标给片元程序
void main() {
gl_Position = uMVPMatrix * vec4(objectPosition, 1.0); //设置物体位置
fragVTexCoord = vTexCoord; //默认无任何处理,直接输出物理内采样坐标
fragObjectColor = objectColor; //默认无任何处理,输出颜色值到片源
}
);
char fragShader[] = GL_SHADER_STRING(
##version 300 es\n
precision highp float;
uniform sampler2D textureFBO;//纹理输入
in vec4 fragObjectColor;//接收vertShader处理后的颜色值给片元程序
in vec2 fragVTexCoord;//接收vertShader处理后的纹理内坐标给片元程序
out vec4 fragColor;//输出到的片元颜色
void main() {
vec4 color = texture(textureFBO, fragVTexCoord);//采样纹理中对应坐标颜色,进行纹理渲染
color.a = color.a * fragObjectColor.a;//利用顶点透明度信息控制纹理透明度
fragColor = color;
}
);
float ratio = (float) mWindowH / mWindowW;;
float tempTexCoord[] = //纹理内采样坐标,类似于canvas坐标 //这东西有问题,导致两个framebuffer的画面互相取纹理时互为颠倒
{
1.0, 0.0,
0.0, 0.0,
1.0, 1.0,
0.0, 1.0
};
memcpy(mTexCoor, tempTexCoord, sizeof(tempTexCoord));
float tempColorBuf[] = {
1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0
};
memcpy(mColorBuf, tempColorBuf, sizeof(tempColorBuf));
float vertxData[] = {
mX + 2, mY, mZ,
mX, mY, mZ,
mX + 2, mY + ratio * 2, mZ,
mX, mY + ratio * 2, mZ,
};
memcpy(mVertxData, vertxData, sizeof(vertxData));
mLayerProgram = createProgram(vertShader + 1, fragShader + 1);
//获取程序中顶点位置属性引用"指针"
mObjectPositionPointer = glGetAttribLocation(mLayerProgram.programHandle, "objectPosition");
//纹理采样坐标
mVTexCoordPointer = glGetAttribLocation(mLayerProgram.programHandle, "vTexCoord");
//获取程序中顶点颜色属性引用"指针"
mObjectVertColorArrayPointer = glGetAttribLocation(mLayerProgram.programHandle, "objectColor");
//获取程序中总变换矩阵引用"指针"
muMVPMatrixPointer = glGetUniformLocation(mLayerProgram.programHandle, "uMVPMatrix");
//创建单位矩阵
initObjMatrix();
}
void Layer::destroy() {
//todo
}
void Layer::addRenderProgram(RenderProgram *program) {
mRenderProgramList.push_back(program);
}
void Layer::removeRenderProgram(RenderProgram *program) {
mRenderProgramList.remove(program);
}
/**给每个模板传入渲染数据**/ //todo 修改一下,如果data更新了才调用第一个渲染器loadData刷新纹理,节约CPU资源 加一个needRefresh标志
void Layer::loadData(char *data, int width, int height, int pixelFormat, int offset) {
mRenderSrcData.data = data;
mRenderSrcData.width = width;
mRenderSrcData.height = height;
mRenderSrcData.pixelFormat = pixelFormat;
mRenderSrcData.offset = offset;
}
/**@param texturePointers 可以用于渲染已经绑定好的纹理,或者直接传入FBO,把上一个图层的结果进一步进行渲染,例如叠加图片、或者进行毛玻璃效果处理。当然也可以在一个图层上叠加更多渲染器实现,但多图层便于不同画面不同大小的重叠,渲染器在同一个图层中大小保持一致**/
void Layer::loadTexture(GLuint texturePointer, int width, int height) {
mRenderSrcTexture.texturePointer = texturePointer;
mRenderSrcTexture.width = width;
mRenderSrcTexture.height = height;
}
void Layer::drawLayerToFrameBuffer(float *cameraMatrix, float *projMatrix, GLuint outputFBOPointer, DrawType drawType) {
glUseProgram(mLayerProgram.programHandle);
glBindFramebuffer(GL_FRAMEBUFFER, outputFBOPointer);
//保留物体缩放现场
float objMatrixClone[16];
memcpy(objMatrixClone, mObjectMatrix, sizeof(objMatrixClone));
//这里的坐标轴逻辑上应该是x,y,z密度都相等的,但因为设备各式各样,x,y轴可能有不同程度的密度拉伸,所以要处理一下:
/**保持图片宽高的原理:
* 0、计算纹理占当前
* 最后贴图时,顶点乘以修改后的物体缩放关系矩阵,就实现了
* **/
if (drawType == DRAW_DATA) {
float ratio =
mWindowW > mWindowH ? ((float) mWindowH / (float) mWindowW) : ((float) mWindowW /
(float) mWindowH); //计算当前视口的短边/长边比例,从而得知X轴和Y轴的-1~1的归一化长度之间的实际长度的比例
//确定图片哪一边更能覆盖对应轴的视口长度,哪一边就让其充满空间,另一边则按OpenGL视口的短边/长边比缩放,此时任意长宽比的图片都会变成矩形,再乘以图片本身的比例转换为图片本身宽高比,即可在纹理渲染时还原图片本身比例
float widthPercentage = (float) mRenderSrcData.width / (float) mWindowW;
float heightPercentage = (float) mRenderSrcData.height / (float) mWindowH;
if (widthPercentage > heightPercentage) { //如果宽占比更多,宽拉伸到尽,高按照视口比例重新调整为统一密度的单位,然后再根据图片高对宽的比例调整物体的高的边的缩放
scale(1.0, ratio * ((float) mRenderSrcData.height / mRenderSrcData.width), 1.0); //SCALEY为图片高占宽的比例 * 视口比例
} else {
scale(ratio * ((float) mRenderSrcData.width / mRenderSrcData.height), 1.0, 1.0);
}
} else {
float ratio =
mWindowW > mWindowH ? ((float) mWindowH / (float) mWindowW) : ((float) mWindowW /
(float) mWindowH); //计算当前视口的短边/长边比例,从而得知X轴和Y轴的-1~1的归一化长度之间的实际长度的比例
//确定图片哪一边更能覆盖对应轴的视口长度,哪一边就让其充满空间,另一边则按OpenGL视口的短边/长边比缩放,此时任意长宽比的图片都会变成矩形,再乘以图片本身的比例转换为图片本身宽高比,即可在纹理渲染时还原图片本身比例
float widthPercentage = (float) mRenderSrcTexture.width / (float) mWindowW;
float heightPercentage = (float) mRenderSrcTexture.height / (float) mWindowH;
if (widthPercentage > heightPercentage) {
scale(1.0, ratio * ((float) mRenderSrcTexture.height / mRenderSrcTexture.width), 1.0);
//另外几种成功的算法:
// scale(1.0, ((float) (mRenderSrcTexture.width * (mWindowH / mRenderSrcTexture.height)) / (float) mWindowW) * ratio, 1.0);
// scale(1.0, ((float) mWindowW / mWindowH) * ((float) mRenderSrcTexture.height / mRenderSrcTexture.width) * (1 / ratio), 1.0);
// scale(1.0, ((float) mRenderSrcTexture.height / mWindowH) * ((float) mWindowW / mRenderSrcTexture.width) * (ratio), 1.0);
} else {
scale(ratio * ((float) mRenderSrcTexture.width / mRenderSrcTexture.height), 1.0, 1.0); //比例式 : 容器w * 纹理
}
}
//物体坐标*缩放平移旋转矩阵->应用按图片比例缩放效果
locationTrans(cameraMatrix, projMatrix, muMVPMatrixPointer);
//还原缩放现场
memcpy(mObjectMatrix, objMatrixClone, sizeof(mObjectMatrix));
// /**实现两个Framebuffer的画面叠加,这里解释一下:
// * 如果是偶数个渲染器,那么在交替渲染之后,那么第0个FBO的画面是上一个画面,第1个FBO为最新画面,所以要先绘制第0个FBO内容再叠加第一个
// * 否则则是交替后,第1个渲染器是上个画面,第0个FBO是上一个画面,叠加顺序则要进行更改**/
// for(int i = 0; i < 2; i ++) {
// glActiveTexture(GL_TEXTURE0);
// if (mRenderProgramList.size() % 2 == 0) {
// setUserScale(0.5, 0.5, 0);
// setUserTransLate(-0.5, 0, 0);
// glBindTexture(GL_TEXTURE_2D, mFrameBufferTexturePointerArray[i]);
// } else {
// setUserScale(0.5, 0.5, 0);
// setUserTransLate(0.5, 0, 0);
// glBindTexture(GL_TEXTURE_2D, mFrameBufferTexturePointerArray[1 - i]);
// }
// glUniform1i(glGetUniformLocation(mLayerProgram.programHandle, "textureFBO"), 0); //获取纹理属性的指针
// //将顶点位置数据送入渲染管线
// glVertexAttribPointer(mObjectPositionPointer, 3, GL_FLOAT, false, 0, mVertxData); //三维向量,size为2
// //将顶点颜色数据送入渲染管线
// glVertexAttribPointer(mObjectVertColorArrayPointer, 4, GL_FLOAT, false, 0, mColorBuf);
// //将顶点纹理坐标数据传送进渲染管线
// glVertexAttribPointer(mVTexCoordPointer, 2, GL_FLOAT, false, 0, mTexCoor); //二维向量,size为2
// glEnableVertexAttribArray(mObjectPositionPointer); //启用顶点属性
// glEnableVertexAttribArray(mObjectVertColorArrayPointer); //启用颜色属性
// glEnableVertexAttribArray(mVTexCoordPointer); //启用纹理采样定位坐标
// glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); //绘制线条,添加的point浮点数/3才是坐标数(因为一个坐标由x,y,z3个float构成,不能直接用)
// glDisableVertexAttribArray(mObjectPositionPointer);
// glDisableVertexAttribArray(mObjectVertColorArrayPointer);
// glDisableVertexAttribArray(mVTexCoordPointer);
// }
/**如果是偶数个fragShaderProgram,则最终画面落于FBO_1。否则奇数时落于FBO_0,并不需要把两个FBO分两次叠加起来**/
glActiveTexture(GL_TEXTURE0);
if (mRenderProgramList.size() % 2 == 0) {
//setUserScale(0.5, 0.5, 0); //测试代码
//setUserTransLate(-0.5, 0, 0); //测试代码
glBindTexture(GL_TEXTURE_2D, mFrameBufferTexturePointerArray[1]);
} else {
//setUserScale(0.5, 0.5, 0); //测试代码
//setUserTransLate(0.5, 0, 0); //测试代码
glBindTexture(GL_TEXTURE_2D, mFrameBufferTexturePointerArray[0]);
}
glUniform1i(glGetUniformLocation(mLayerProgram.programHandle, "textureFBO"), 0); //获取纹理属性的索引,给索引对应变量赋当前ActiveTexture的索引
//将顶点位置数据送入渲染管线
glVertexAttribPointer(mObjectPositionPointer, 3, GL_FLOAT, false, 0, mVertxData); //三维向量,size为2
//将顶点颜色数据送入渲染管线
glVertexAttribPointer(mObjectVertColorArrayPointer, 4, GL_FLOAT, false, 0, mColorBuf);
//将顶点纹理坐标数据传送进渲染管线
glVertexAttribPointer(mVTexCoordPointer, 2, GL_FLOAT, false, 0, mTexCoor); //二维向量,size为2
glEnableVertexAttribArray(mObjectPositionPointer); //启用顶点属性
glEnableVertexAttribArray(mObjectVertColorArrayPointer); //启用颜色属性
glEnableVertexAttribArray(mVTexCoordPointer); //启用纹理采样定位坐标
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); //绘制线条,添加的point浮点数/3才是坐标数(因为一个坐标由x,y,z3个float构成,不能直接用)
glDisableVertexAttribArray(mObjectPositionPointer);
glDisableVertexAttribArray(mObjectVertColorArrayPointer);
glDisableVertexAttribArray(mVTexCoordPointer);
}
/**逐步加工绘制
* @param cameraMatrix 摄像机矩阵,确定观察者的位置、观察画面的旋转程度和观察方向
* @param projMatrix 投影矩阵,决定3d物体通过怎样的系数投影到屏幕**/ //todo 修改一下,如果data更新了才调用第一个渲染器loadData刷新纹理,节约CPU资源
void
Layer::drawTo(float *cameraMatrix, float *projMatrix, GLuint outputFBOPointer, int fboW, int fboH, DrawType drawType) {
//清理双Framebuffer残留的内容
for (int i = 0; i < 2; i++) {
glBindFramebuffer(GL_FRAMEBUFFER, mFrameBufferPointerArray[i]);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); //清理屏幕
}
int i = 0;
/**第0个渲染器以data为数据输入,使用FBO[0]渲染结果。第1个渲染器使用FBO_texture[0]作为纹理输入,渲染结果输出到FBO[1]。
* 第2个渲染器使用FBO_texture[1]作为纹理输入,渲染结果输出到FBO[0],依次循环互换结果和输入,实现效果叠加。
* 使用双FBO互为绑定的原因是为了解决部分shader算法如果绑定的FBO_texture和输出的FBO是同一个将会出现异常,所以使用此方法**/
for (auto item = mRenderProgramList.begin(); item != mRenderProgramList.end(); item++, i++) {
//接收绘制数据的framebuffer和作为纹理输入使用的framebuffer不能是同一个
int layerFrameBuffer = i % 2 == 0 ? mFrameBufferPointerArray[0] : mFrameBufferPointerArray[1];
int fboTexture = i % 2 == 1 ? mFrameBufferTexturePointerArray[0] : mFrameBufferTexturePointerArray[1];
//第一个渲染器接受图层原始数据(图层原始数据可以是输出的字节数组,或是纹理本身,例如OES纹理),其他的从上一个渲染结果中作为输入
if (i == 0) {
switch (drawType) {
case DRAW_DATA: {
if (mRenderSrcData.data != nullptr) {
(*item)->loadData(mRenderSrcData.data, mRenderSrcData.width,
mRenderSrcData.height,
mRenderSrcData.pixelFormat, mRenderSrcData.offset);
//渲染器处理结果放到图层FBO中
(*item)->drawTo(cameraMatrix, projMatrix, RenderProgram::DRAW_DATA,
layerFrameBuffer, mWindowW, mWindowH);
}
break;
}
case DRAW_TEXTURE: {
Textures texture;
texture.texturePointers = mRenderSrcTexture.texturePointer;
texture.width = mRenderSrcTexture.width;
texture.height = mRenderSrcTexture.height;
Textures textures[1];
textures[0] = texture;
(*item)->loadTexture(textures);
//渲染器处理结果放到图层FBO中
(*item)->drawTo(cameraMatrix, projMatrix, RenderProgram::DRAW_TEXTURE,
layerFrameBuffer, mWindowW, mWindowH);
break;
}
}
} else { //如果只有一个渲染器则走不到else里,否则第0个打后的渲染器依次使用上一个渲染器的结果,也就是图层FBO中的数据作为输入 bug
//使用上一个渲染器保存到FBO的结果,也就是FBO_texture作为纹理输入进行二次处理
Textures t[1];
t[0].texturePointers = fboTexture;
t[0].width = mWindowW;
t[0].height = mWindowH;
(*item)->loadTexture(t); //使用上一个渲染器的渲染结果作为绘制输入
//渲染器处理结果放到图层FBO中
(*item)->drawTo(cameraMatrix, projMatrix, RenderProgram::DRAW_TEXTURE,
layerFrameBuffer, mWindowW, mWindowH);
}
}
//最后渲染到目标framebuffer
drawLayerToFrameBuffer(cameraMatrix, projMatrix, outputFBOPointer, drawType);
//渲染统计
mFrameCount++;
}
其中createFrameBuffer创建了两个FBO对象,参考的技术是双缓冲机制,每次进行当前fragShaderProgram渲染时,FBO_0保存了上一个fragShaderProgram的渲染结果,作为纹理输入到当前正在执行的fragShaderProgram,FBO_1用于保存当前fragShaderProgram渲染的结果。执行下一个fragShaderProgram时,FBO_1作为输入,FBO_0作为输出,依次类推,每渲染一次就交换一次。实现单个图层多重效果的渲染。
其中drawTo方法可以传入用于承载图层内容的FBO,默认就是0号FBO,也就是glSurfaceview在创建EGLContext的时候并绑定到view上的那个,使得内容可以呈现于屏幕上。其中比较重要部分的代码如下:
for (auto item = mRenderProgramList.begin(); item != mRenderProgramList.end(); item++, i++) {
//接收绘制数据的framebuffer和作为纹理输入使用的framebuffer不能是同一个
int layerFrameBuffer = i % 2 == 0 ? mFrameBufferPointerArray[0] : mFrameBufferPointerArray[1];
int fboTexture = i % 2 == 1 ? mFrameBufferTexturePointerArray[0] : mFrameBufferTexturePointerArray[1];
//第一个渲染器接受图层原始数据(图层原始数据可以是输出的字节数组,或是纹理本身,例如OES纹理),其他的从上一个渲染结果中作为输入
if (i == 0) {
switch (drawType) {
case DRAW_DATA: {
if (mRenderSrcData.data != nullptr) {
(*item)->loadData(mRenderSrcData.data, mRenderSrcData.width,
mRenderSrcData.height,
mRenderSrcData.pixelFormat, mRenderSrcData.offset);
//渲染器处理结果放到图层FBO中
(*item)->drawTo(cameraMatrix, projMatrix, RenderProgram::DRAW_DATA,
layerFrameBuffer, mWindowW, mWindowH);
}
break;
}
case DRAW_TEXTURE: {
Textures texture;
texture.texturePointers = mRenderSrcTexture.texturePointer;
texture.width = mRenderSrcTexture.width;
texture.height = mRenderSrcTexture.height;
Textures textures[1];
textures[0] = texture;
(*item)->loadTexture(textures);
//渲染器处理结果放到图层FBO中
(*item)->drawTo(cameraMatrix, projMatrix, RenderProgram::DRAW_TEXTURE,
layerFrameBuffer, mWindowW, mWindowH);
break;
}
}
} else { //如果只有一个渲染器则走不到else里,否则第0个打后的渲染器依次使用上一个渲染器的结果,也就是图层FBO中的数据作为输入 bug
//使用上一个渲染器保存到FBO的结果,也就是FBO_texture作为纹理输入进行二次处理
Textures t[1];
t[0].texturePointers = fboTexture;
t[0].width = mWindowW;
t[0].height = mWindowH;
(*item)->loadTexture(t); //使用上一个渲染器的渲染结果作为绘制输入
//渲染器处理结果放到图层FBO中
(*item)->drawTo(cameraMatrix, projMatrix, RenderProgram::DRAW_TEXTURE,
layerFrameBuffer, mWindowW, mWindowH);
}
}
之前介绍createFrameBuffer的设计原理时有说过其使用了双缓冲机制,所以fragShaderProgram的数目的奇偶性的不同,图层渲染的最终画面可能会落入不同的Framebuffer中。例如图层的渲染流水线有3个fragShaderProgram的话,那么就是先渲染画面输出到了Framebuffer0,再把framebuffer0作为输入,渲染后输出到Framebuffer1,然后再把Framebuffer1作为输入,输出到Framebuffer0。这种轮换模式,使得fragShaderProgram的渲染效果可以不停叠加。
renderProgram的基类设计如下:
//
// Created by jiezhuchen on 2021/6/21.
//
#include
#include
#include "RenderProgram.h"
#include "matrix.c"
#include "shaderUtil.c"
using namespace OPENGL_VIDEO_RENDERER;
void RenderProgram::initObjMatrix() {
//创建单位矩阵
setIdentityM(mObjectMatrix, 0);
}
void RenderProgram::scale(float sx, float sy, float sz) {
scaleM(mObjectMatrix, 0, sx, sy, sz);
}
void RenderProgram::translate(float dx, float dy, float dz) {
translateM(mObjectMatrix, 0, dx, dy, dz);
}
void RenderProgram::rotate(int degree, float roundX, float roundY, float roundZ) {
rotateM(mObjectMatrix, 0, degree, roundX, roundY, roundZ);
}
float* RenderProgram::getObjectMatrix() {
return mObjectMatrix;
}
void RenderProgram::setObjectMatrix(float objMatrix[]) {
memcpy(mObjectMatrix, objMatrix, sizeof(mObjectMatrix));
}
void RenderProgram::locationTrans(float cameraMatrix[], float projMatrix[], int muMVPMatrixPointer) {
multiplyMM(mMVPMatrix, 0, cameraMatrix, 0, mObjectMatrix, 0); //将摄像机矩阵乘以物体矩阵
multiplyMM(mMVPMatrix, 0, projMatrix, 0, mMVPMatrix, 0); //将投影矩阵乘以上一步的结果矩阵
glUniformMatrix4fv(muMVPMatrixPointer, 1, false, mMVPMatrix); //将最终变换关系传入渲染管线
}
然后继承之后,按照自己的需要,实现自己的渲染流水线,例子如下(LUT滤镜渲染器):
//
// Created by jiezhuchen on 2021/6/21.
//
#include
#include
#include
#include
#include
#include "RenderProgramFilter.h"
#include "android/log.h"
using namespace OPENGL_VIDEO_RENDERER;
static const char *TAG = "nativeGL";
#define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, ##args)
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args)
#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)
RenderProgramFilter::RenderProgramFilter() {
vertShader = GL_SHADER_STRING(
$#version 300 es\n
uniform mat4 uMVPMatrix; //旋转平移缩放 总变换矩阵。物体矩阵乘以它即可产生变换
in vec3 objectPosition; //物体位置向量,参与运算但不输出给片源
in vec4 objectColor; //物理颜色向量
in vec2 vTexCoord; //纹理内坐标
out vec4 fragObjectColor;//输出处理后的颜色值给片元程序
out vec2 fragVTexCoord;//输出处理后的纹理内坐标给片元程序
void main() {
gl_Position = uMVPMatrix * vec4(objectPosition, 1.0); //设置物体位置
fragVTexCoord = vTexCoord; //默认无任何处理,直接输出物理内采样坐标
fragObjectColor = objectColor; //默认无任何处理,输出颜色值到片源
}
);
fragShader = GL_SHADER_STRING(
##version 300 es\n
precision highp float;
precision highp sampler2DArray;
uniform sampler2D sTexture;//图像纹理输入
uniform sampler2DArray lutTexture;//滤镜纹理输入
uniform float pageSize;
uniform float frame;//第几帧
uniform vec2 resolution;//分辨率
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)));
}
);
float tempTexCoord[] = //纹理内采样坐标,类似于canvas坐标 //这东西有问题,导致两个framebuffer的画面互相取纹理时互为颠倒
{
1.0, 0.0,
0.0, 0.0,
1.0, 1.0,
0.0, 1.0
};
memcpy(mTexCoor, tempTexCoord, sizeof(tempTexCoord));
float tempColorBuf[] = {
1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0
};
memcpy(mColorBuf, tempColorBuf, sizeof(tempColorBuf));
}
RenderProgramFilter::~RenderProgramFilter() {
destroy();
}
void RenderProgramFilter::createRender(float x, float y, float z, float w, float h, int windowW,
int windowH) {
mWindowW = windowW;
mWindowH = windowH;
initObjMatrix(); //使物体矩阵初始化为单位矩阵,否则接下来的矩阵操作因为都是乘以0而无效
float vertxData[] = {
x + w, y, z,
x, y, z,
x + w, y + h, z,
x, y + h, z,
};
memcpy(mVertxData, vertxData, sizeof(vertxData));
mImageProgram = createProgram(vertShader + 1, fragShader + 1);
//获取程序中顶点位置属性引用"指针"
mObjectPositionPointer = glGetAttribLocation(mImageProgram.programHandle, "objectPosition");
//纹理采样坐标
mVTexCoordPointer = glGetAttribLocation(mImageProgram.programHandle, "vTexCoord");
//获取程序中顶点颜色属性引用"指针"
mObjectVertColorArrayPointer = glGetAttribLocation(mImageProgram.programHandle, "objectColor");
//获取程序中总变换矩阵引用"指针"
muMVPMatrixPointer = glGetUniformLocation(mImageProgram.programHandle, "uMVPMatrix");
//渲染方式选择,0为线条,1为纹理
mGLFunChoicePointer = glGetUniformLocation(mImageProgram.programHandle, "funChoice");
//渲染帧计数指针
mFrameCountPointer = glGetUniformLocation(mImageProgram.programHandle, "frame");
//设置分辨率指针,告诉gl脚本现在的分辨率
mResoulutionPointer = glGetUniformLocation(mImageProgram.programHandle, "resolution");
}
void RenderProgramFilter::setAlpha(float alpha) {
if (mColorBuf != nullptr) {
for (int i = 3; i < sizeof(mColorBuf) / sizeof(float); i += 4) {
mColorBuf[i] = alpha;
}
}
}
//todo
void RenderProgramFilter::setBrightness(float brightness) {
}
//todo
void RenderProgramFilter::setContrast(float contrast) {
}
//todo
void RenderProgramFilter::setWhiteBalance(float redWeight, float greenWeight, float blueWeight) {
}
void RenderProgramFilter::loadData(char *data, int width, int height, int pixelFormat, int offset) {
if (!mIsTexutresInited) {
glUseProgram(mImageProgram.programHandle);
glGenTextures(1, mTexturePointers);
mGenTextureId = mTexturePointers[0];
mIsTexutresInited = true;
}
//绑定处理
glBindTexture(GL_TEXTURE_2D, mGenTextureId);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, pixelFormat, width, height, 0, pixelFormat, GL_UNSIGNED_BYTE, (void*) (data + offset));
mDataWidth = width;
mDataHeight = height;
}
/**@param texturePointers 传入需要渲染处理的纹理,可以为上一次处理的结果,例如处理完后的FBOTexture **/
void RenderProgramFilter::loadTexture(Textures textures[]) {
mInputTexturesArrayPointer = textures[0].texturePointers;
mInputTextureWidth = textures[0].width;
mInputTextureHeight = textures[0].height;
}
/**设置LUT滤镜**/
void RenderProgramFilter::loadLut(char* lutPixels, int lutWidth, int lutHeight, int unitLength) { //纹理更新只能在GL线程里面操作,所以这里只能先保存一下数据
mLutWidth = lutWidth;
mLutHeight = lutHeight;
mLutUnitLen = unitLength;
mLutPixels = (char*) malloc(mLutWidth * mLutHeight * 4);
memcpy(mLutPixels, lutPixels, mLutWidth * mLutHeight * 4);
}
/**@param outputFBOPointer 绘制到哪个framebuffer,系统默认一般为0 **/
void RenderProgramFilter::drawTo(float *cameraMatrix, float *projMatrix, DrawType drawType, int outputFBOPointer, int fboW, int fboH) {
if (mIsDestroyed) {
return;
}
glUseProgram(mImageProgram.programHandle);
//设置视窗大小及位置
glBindFramebuffer(GL_FRAMEBUFFER, outputFBOPointer);
glViewport(0, 0, mWindowW, mWindowH);
glUniform1i(mGLFunChoicePointer, 1);
//传入位置信息
locationTrans(cameraMatrix, projMatrix, muMVPMatrixPointer);
//开始渲染:
if (mVertxData != nullptr && mColorBuf != nullptr) {
//将顶点位置数据送入渲染管线
glVertexAttribPointer(mObjectPositionPointer, 3, GL_FLOAT, false, 0, mVertxData); //三维向量,size为2
//将顶点颜色数据送入渲染管线
glVertexAttribPointer(mObjectVertColorArrayPointer, 4, GL_FLOAT, false, 0, mColorBuf);
//将顶点纹理坐标数据传送进渲染管线
glVertexAttribPointer(mVTexCoordPointer, 2, GL_FLOAT, false, 0, mTexCoor); //二维向量,size为2
glEnableVertexAttribArray(mObjectPositionPointer); //启用顶点属性
glEnableVertexAttribArray(mObjectVertColorArrayPointer); //启用颜色属性
glEnableVertexAttribArray(mVTexCoordPointer); //启用纹理采样定位坐标
float resolution[2];
switch (drawType) {
case OPENGL_VIDEO_RENDERER::RenderProgram::DRAW_DATA:
glActiveTexture(GL_TEXTURE0); //激活0号纹理
glBindTexture(GL_TEXTURE_2D, mGenTextureId); //0号纹理绑定内容
glUniform1i(glGetUniformLocation(mImageProgram.programHandle, "sTexture"), 0); //映射到渲染脚本,获取纹理属性的指针
resolution[0] = (float) mDataWidth;
resolution[1] = (float) mDataHeight;
glUniform2fv(mResoulutionPointer, 1, resolution);
break;
case OPENGL_VIDEO_RENDERER::RenderProgram::DRAW_TEXTURE:
glActiveTexture(GL_TEXTURE0); //激活0号纹理
glBindTexture(GL_TEXTURE_2D, mInputTexturesArrayPointer); //0号纹理绑定内容
glUniform1i(glGetUniformLocation(mImageProgram.programHandle, "sTexture"), 0); //映射到渲染脚本,获取纹理属性的指针
resolution[0] = (float) mInputTextureWidth;
resolution[1] = (float) mInputTextureHeight;
glUniform2fv(mResoulutionPointer, 1, resolution);
break;
}
int longLen = mLutWidth > mLutHeight ? mLutWidth : mLutHeight;
if (mLutPixels) {
if (mHadLoadLut) {
glBindTexture(GL_TEXTURE_2D_ARRAY, mLutTexutresPointers[0]);
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, 0, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE,
nullptr);
glDeleteTextures(1, mLutTexutresPointers);
}
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);
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, mLutUnitLen, mLutUnitLen, longLen / mLutUnitLen, 0, GL_RGBA, GL_UNSIGNED_BYTE, mLutPixels);
//lut数据加载完毕,清理内存
free(mLutPixels);
mLutPixels = nullptr;
mHadLoadLut = true;
}
if (mHadLoadLut) {
glActiveTexture(GL_TEXTURE1); //激活1号纹理
glBindTexture(GL_TEXTURE_2D_ARRAY, mLutTexutresPointers[0]);
glUniform1i(glGetUniformLocation(mImageProgram.programHandle, "lutTexture"), 1); //映射到渲染脚本,获取纹理属性的指针
glUniform1f(glGetUniformLocation(mImageProgram.programHandle, "pageSize"), longLen / mLutUnitLen); //映射到渲染脚本,获取纹理属性的指针
}
glDrawArrays(GL_TRIANGLE_STRIP, 0, /*mPointBufferPos / 3*/ 4); //绘制线条,添加的point浮点数/3才是坐标数(因为一个坐标由x,y,z3个float构成,不能直接用)
glDisableVertexAttribArray(mObjectPositionPointer);
glDisableVertexAttribArray(mObjectVertColorArrayPointer);
glDisableVertexAttribArray(mVTexCoordPointer);
}
}
void RenderProgramFilter::destroy() {
if (!mIsDestroyed) {
//释放纹理所占用的显存
glBindTexture(GL_TEXTURE_2D, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, 0, 0, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, nullptr);
glDeleteTextures(1, mTexturePointers);
glBindTexture(GL_TEXTURE_2D_ARRAY, mLutTexutresPointers[0]);
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, 0, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE,
nullptr);
glDeleteTextures(1, mLutTexutresPointers);
//删除不用的shaderprogram
destroyProgram(mImageProgram);
}
mIsDestroyed = true;
}
简单说一下Layer和自己写的render如何搭配使用:
1、创建图层,输入需要处理的纹理或者数据:
Layer *layer = new Layer(-1, -mRatio, 0, 2, mRatio * 2, mWidth, mHeight); //创建铺满全屏的图层;
//载入数据:
LOGI("cjztest, Java_com_opengldecoder_jnibridge_JniBridge_addFullContainerLayer containerW:%d, containerH:%d, w:%d, h:%d", mWidth, mHeight, textureWidthAndHeightPointer[0], textureWidthAndHeightPointer[1]);
layer->loadTexture(texturePointer, textureWidthAndHeightPointer[0], textureWidthAndHeightPointer[1]);
layer->loadData((char *) dataPointer, dataWidthAndHeightPointer[0], dataWidthAndHeightPointer[1], dataPixelFormat, 0);
if (mLayerList) {
struct ListElement* cursor = mLayerList;
while (cursor->next) {
cursor = cursor->next;
}
cursor->next = (struct ListElement*) malloc(sizeof(struct ListElement));
cursor->next->layer = layer;
cursor->next->next = nullptr;
} else {
mLayerList = (struct ListElement*) malloc(sizeof(struct ListElement));
mLayerList->layer = layer;
mLayerList->next = nullptr;
}
2、对图层添加想要的fragShader渲染器
RenderProgramFilter *renderProgramFilter = new RenderProgramFilter();
renderProgramFilter->createRender(-1, -mRatio, 0, 2,
mRatio * 2,
mWidth,
mHeight);
resultProgram = renderProgramFilter;
layer->addRenderProgram(resultProgram);
3、输入画面呈现用的framebuffer索引
//防止画面残留:
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); //清理屏幕
glClearColor(0.0, 0.0, 0.0, 0.0);
//遍历图层并渲染
if (mLayerList) {
struct ListElement* cursor = mLayerList;
while (cursor) {
cursor->layer->drawTo(mCameraMatrix, mProjMatrix, fboPointer, fboWidth, fboHeight, Layer::DRAW_TEXTURE);
cursor = cursor->next;
}
}
Demo代码地址:
learnOpengl: 我的OpenGL联系库 - Gitee.comhttps://gitee.com/cjzcjl/learnOpenGLDemo/tree/main/app/src/main/cpp/opengl_decoder
实际效果,这个Demo依次开启了卷积和LUT滤镜两个流水线fragShader:
在安卓上基于OpenGL ES实现渲染流水线和Lut滤镜,效果展示
至此,一个简单的OpenGL 多图层多重渲染流水线的简易框架就搭建完毕,可以在此之上扩充为一个视频编辑软件或者图像处理软件。