学习动机:
问题:最近在做直播项目礼物动画,要求mp4格式文件进行播放,由于MP4视频帧只有RGB信息,没有透明度信息,所以不能实现在直播间内透明播放,导致播放时后面直播内容被遮挡。
-
思路:最后想到可以通过合成视频画面的方式达到视频背景透明的效果,原理就是视频文件包含两部分,一部分是原视频内容,一部分是原视频的黑白内容,使用黑白部分内容(0xFFFFFF表示alpha 1.0,0x000000表示alpha 0)对原视频部分附加透明度,进而实现视频播放的透明背景效果。
实现过程
1,捕获到原视频的每帧画面CVPixelBufferRef,内容及上图。
2,使用OpenGL进行处理,为每个画面附加透明度,最后渲染到屏幕上。
一、OpenGL是什么
全名是open graphics library , 用于渲染2d,3d图像的跨平台,跨语言的应用程序编程接口
二、OpenGL 能做什么?
可以对图像进行各种美颜,滤镜,裁剪,贴纸等处理,源图像数据可以是来自相机,文件,图片等。GPUImage框架底层就是用opengl实现的
三、利用OpenGL渲染帧数据并显示
导入头文件#import <GLKit/GLKit.h>,GLKit.h底层使用了OpenGLES,导入它,相当于自动导入了OpenGLES
步骤
01-自定义图层类型
02-初始化CAEAGLLayer图层属性
03-创建EAGLContext
04-创建渲染缓冲区
05-创建帧缓冲区
06-创建着色器
07-创建着色器程序
08-创建纹理对象
09-YUV转RGB绘制纹理
10-渲染缓冲区到屏幕
11-清理内存
01自定义图层类型
//CAEAGLLayer是OpenGL专门用来渲染的图层,
//使用OpenGL必须使用这个图层
+ (Class)layerClass {
return [CAEAGLLayer class];
}
02初始化CAEAGLLayer图层属性
//设置图层
CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer;
eaglLayer.opaque = NO; //这个一定要设置 不然无法透明
eaglLayer.backgroundColor = [UIColor clearColor].CGColor;
/*kEAGLDrawablePropertyRetainedBacking 是否需要保留已经绘制到图层上面的内容
kEAGLDrawablePropertyColorFormat 绘制对象内部的颜色缓冲区的格式 kEAGLColorFormatRGBA8 4*8 = 32*/
eaglLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking : [NSNumber numberWithBool:NO],
kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8};
03-创建EAGLContext
//设置上下文
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:context];
04-创建渲染缓冲区
//创建渲染缓存
glGenRenderbuffers(1, colorBufferHandle);
glBindRenderbuffer(GL_RENDERBUFFER, *colorBufferHandle);
// 把渲染缓存绑定到渲染图层上CAEAGLLayer,并为它分配一个共享内存。
// 并且会设置渲染缓存的格式,和宽度
[context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];
05-创建帧缓冲区
//创建帧缓存
glGenFramebuffers(1, frameBufferHandle);
glBindFramebuffer(GL_FRAMEBUFFER, *frameBufferHandle);
// 把颜色渲染缓存 添加到 帧缓存的GL_COLOR_ATTACHMENT0上,就会自动把渲染缓存的内容填充到帧缓存,在由帧缓存渲染到屏幕
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, *colorBufferHandle);
06-创建着色器
什么是着色器?
通常用来处理纹理对象,并且把处理好的纹理对象渲染到帧缓存上,从而显示到屏幕上。
提取纹理信息,可以处理顶点坐标空间转换,纹理色彩度调整(滤镜效果)等操作。着色器分为顶点着色器,片段着色器
顶点着色器用来确定图形形状。
片段着色器用来确定图形渲染颜色。步骤: 1.编辑着色器代码 2.创建着色器 3.编译着色器
只要创建一次,可以在一开始的时候创建编辑着色器代码 :glsl语言,应该属于gpu编程,如下:
//顶点着色器代码
attribute vec4 Position;
attribute vec4 TextureCoords;
varying vec2 TextureCoordsVarying;
void main (void) {
gl_Position = Position;
TextureCoordsVarying = TextureCoords.xy;
}
//片段着色器代码
precision mediump float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
void main (void) {
vec4 mask = texture2D(Texture, TextureCoordsVarying);
vec4 alpha = texture2D(Texture, TextureCoordsVarying + vec2(-0.5, 0.0));
gl_FragColor = vec4(mask.rgb, alpha.r);
}
- 创建 +编译:属于动态编译
GLint status;
//sourceString 是顶点or片段着色器 的代码文本
const GLchar *source;
source = (GLchar *)[sourceString UTF8String];
//type 顶点or片段着色器
*shader = glCreateShader(type); // 创建着色器
glShaderSource(*shader, 1, &source, NULL);//加载着色器源代码
glCompileShader(*shader); //编译着色器
glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);//获取完成状态
if (status == 0) {
// 没有完成就直接删除着色器
glDeleteShader(*shader);
return NO;
}
07-创建着色器程序
//创建一个着色器程序对象
GLuint program = glCreateProgram();
_rgbProgram = program;
//关联着色器对象到着色器程序对象
//绑定顶点着色器
glAttachShader(program, vertShader);
//绑定片元着色器
glAttachShader(program, fragShader);
// 绑定着色器属性,方便以后获取,以后根据角标获取
// 一定要在链接程序之前绑定属性,否则拿不到
glBindAttribLocation(program, ATTRIB_VERTEX , "Position");
glBindAttribLocation(program, ATTRIB_TEXCOORD, "TextureCoords");
//链接程序
if (![self linkProgram:program]) {
//链接失败释放vertShader\fragShader\program
if (vertShader) {
glDeleteShader(vertShader);
vertShader = 0;
}
if (fragShader) {
glDeleteShader(fragShader);
fragShader = 0;
}
if (program) {
glDeleteProgram(program);
program = 0;
}
return;
}
/// 获取全局参数,注意 一定要在连接完成后才行,否则拿不到
_displayInputTextureUniform = glGetUniformLocation(program, "Texture");
//释放已经使用完毕的verShader\fragShader
if (vertShader) {
glDetachShader(program, vertShader);
glDeleteShader(vertShader);
}
if (fragShader) {
glDetachShader(program, fragShader);
glDeleteShader(fragShader);
}
//启动程序
glUseProgram(rgbProgram);
08-创建纹理对象
通过获取的一张一张的图片(CVPixelBufferRef pixelBuffer),可以把图片转换为OpenGL中的纹理, 然后再把纹理画到OpenGL的上下文中
什么是纹理?一个纹理其实就是一幅图像。
纹理映射,我们可以把这幅图像的整体或部分贴到我们先前用顶点勾画出的物体上去.
比如绘制一面砖墙,就可以用一幅真实的砖墙图像或照片作为纹理贴到一个矩形上,这样,一面逼真的砖墙就画好了。如果不用纹理映射的方法,则墙上的每一块砖都必须作为一个独立的多边形来画。另外,纹理映射能够保证在变换多边形时,多边形上的纹理图案也随之变化。
纹理映射是一个相当复杂的过程,基本步骤如下:
1)激活纹理单元、2)创建纹理 、3)绑定纹理 、4)设置滤波
注意:纹理映射只能在RGBA方式下执行
// 创建亮度纹理
// 激活纹理单元0, 不激活,创建纹理会失败
glActiveTexture(GL_TEXTURE0);
// 创建纹理对象
CVReturn error;
error = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
videoTextureCache,
pixelBuffer,
NULL,
GL_TEXTURE_2D,
GL_RGBA,
frameWidth,
frameHeight,
GL_BGRA,
GL_UNSIGNED_BYTE,
0,
&renderTexture);
if (error) {
NSLog(@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", error);
}else {
_renderTexture = renderTexture;
}
//获取纹理对象 CVOpenGLESTextureGetName(renderTexture)
//绑定纹理
glBindTexture(CVOpenGLESTextureGetTarget(renderTexture), CVOpenGLESTextureGetName(renderTexture));
//设置纹理滤波
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(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);
09-绘制纹理
如果源视频色彩空间是yuv格式则需要转为grb格式,一般直播推流时使用的是yuv格式,此处我是播放的静态mp4文件,所以略过格式转换
// 在创建纹理之前,有激活过纹理单元,glActiveTexture(GL_TEXTURE0)
// 指定着色器中亮度纹理对应哪一层纹理单元
// 这样就会把亮度纹理,往着色器上贴
glUniform1i(displayInputTextureUniform, 0);
if (self.pixelbufferWidth != frameWidth || self.pixelbufferHeight != frameHeight) {
CGSize normalizedSamplingSize = CGSizeMake(1.0, 1.0);
self.pixelbufferWidth = frameWidth;
self.pixelbufferHeight = frameHeight;
/*
//纹理坐标
GLfloat quadTextureData[] = {
0.5f, 1.0f,
0.5f, 0.0f,
1.0f, 1.0f,
1.0f, 0.0f,
};
//顶点坐标
GLfloat quadVertexData[] = {
-1.0f, 1.0f,
-1.0f, -1.0f,
1.0f, 1.0f,
1.0f, -1.0f,
};
*/
// 左下角
quadVertexData[0] = -1 * normalizedSamplingSize.width;
quadVertexData[1] = -1 * normalizedSamplingSize.height;
// 左上角
quadVertexData[2] = -1 * normalizedSamplingSize.width;
quadVertexData[3] = normalizedSamplingSize.height;
// 右下角
quadVertexData[4] = normalizedSamplingSize.width;
quadVertexData[5] = -1 * normalizedSamplingSize.height;
// 右上角
quadVertexData[6] = normalizedSamplingSize.width;
quadVertexData[7] = normalizedSamplingSize.height;
}
//激活ATTRIB_VERTEX顶点数组
glEnableVertexAttribArray(ATTRIB_VERTEX);
//给ATTRIB_VERTEX顶点数组赋值
glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, quadVertexData);
//激活ATTRIB_TEXCOORD顶点数组
glEnableVertexAttribArray(ATTRIB_TEXCOORD);
//给ATTRIB_TEXCOORD顶点数组赋值
glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, 0, 0, quadTextureData);
//渲染纹理数据
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
10-渲染缓冲区到屏幕
- (void)displayFramebuffer:(CMSampleBufferRef)sampleBuffer{
// 因为是多线程,每一个线程都有一个上下文,只要在一个上下文绘制就好,设置线程的上下文为我们自己的上下文,就能绘制在一起了,否则会黑屏.
if ([EAGLContext currentContext] != _context) {
[EAGLContext setCurrentContext:_context];
}
// 清空之前的纹理,要不然每次都创建新的纹理,耗费资源,造成界面卡顿
[self cleanUpTextures];
//执行第8步 创建纹理对象
//设置视口大小
glViewport(0, 0, backingWidth, backingHeight);
//设置一个RGB颜色和透明度,接下来会用这个颜色涂满全屏
glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
//清除颜色缓冲区
glClear(GL_COLOR_BUFFER_BIT);
//执行第9步 绘制纹理,也就是着色器提取并处理纹理
/// 把上下文的东西渲染到屏幕上
if ([EAGLContext currentContext] == context) {
[context presentRenderbuffer:GL_RENDERBUFFER];
}
}
清理内存
- (void)cleanUpTextures {
if (_renderTexture) {
CFRelease(_renderTexture);
_renderTexture = NULL;
}
// 清空纹理缓存
CVOpenGLESTextureCacheFlush(_videoTextureCache, 0);
}
- (void)dealloc {
[self cleanUpTextures];
if(_videoTextureCache) {
CFRelease(_videoTextureCache);
}
}