學(xué)習(xí)動(dòng)機(jī):
問(wèn)題:最近在做直播項(xiàng)目禮物動(dòng)畫,要求mp4格式文件進(jìn)行播放,由于MP4視頻幀只有RGB信息,沒(méi)有透明度信息,所以不能實(shí)現(xiàn)在直播間內(nèi)透明播放,導(dǎo)致播放時(shí)后面直播內(nèi)容被遮擋。
-
思路:最后想到可以通過(guò)合成視頻畫面的方式達(dá)到視頻背景透明的效果,原理就是視頻文件包含兩部分,一部分是原視頻內(nèi)容,一部分是原視頻的黑白內(nèi)容,使用黑白部分內(nèi)容(0xFFFFFF表示alpha 1.0,0x000000表示alpha 0)對(duì)原視頻部分附加透明度,進(jìn)而實(shí)現(xiàn)視頻播放的透明背景效果。
image.png 實(shí)現(xiàn)過(guò)程
1,捕獲到原視頻的每幀畫面CVPixelBufferRef,內(nèi)容及上圖。
2,使用OpenGL進(jìn)行處理,為每個(gè)畫面附加透明度,最后渲染到屏幕上。
一、OpenGL是什么
全名是open graphics library , 用于渲染2d,3d圖像的跨平臺(tái),跨語(yǔ)言的應(yīng)用程序編程接口
二、OpenGL 能做什么?
可以對(duì)圖像進(jìn)行各種美顏,濾鏡,裁剪,貼紙等處理,源圖像數(shù)據(jù)可以是來(lái)自相機(jī),文件,圖片等。GPUImage框架底層就是用opengl實(shí)現(xiàn)的
三、利用OpenGL渲染幀數(shù)據(jù)并顯示
導(dǎo)入頭文件#import <GLKit/GLKit.h>,GLKit.h底層使用了OpenGLES,導(dǎo)入它,相當(dāng)于自動(dòng)導(dǎo)入了OpenGLES
步驟
01-自定義圖層類型
02-初始化CAEAGLLayer圖層屬性
03-創(chuàng)建EAGLContext
04-創(chuàng)建渲染緩沖區(qū)
05-創(chuàng)建幀緩沖區(qū)
06-創(chuàng)建著色器
07-創(chuàng)建著色器程序
08-創(chuàng)建紋理對(duì)象
09-YUV轉(zhuǎn)RGB繪制紋理
10-渲染緩沖區(qū)到屏幕
11-清理內(nèi)存
01自定義圖層類型
//CAEAGLLayer是OpenGL專門用來(lái)渲染的圖層,
//使用OpenGL必須使用這個(gè)圖層
+ (Class)layerClass {
return [CAEAGLLayer class];
}
02初始化CAEAGLLayer圖層屬性
//設(shè)置圖層
CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer;
eaglLayer.opaque = NO; //這個(gè)一定要設(shè)置 不然無(wú)法透明
eaglLayer.backgroundColor = [UIColor clearColor].CGColor;
/*kEAGLDrawablePropertyRetainedBacking 是否需要保留已經(jīng)繪制到圖層上面的內(nèi)容
kEAGLDrawablePropertyColorFormat 繪制對(duì)象內(nèi)部的顏色緩沖區(qū)的格式 kEAGLColorFormatRGBA8 4*8 = 32*/
eaglLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking : [NSNumber numberWithBool:NO],
kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8};
03-創(chuàng)建EAGLContext
//設(shè)置上下文
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:context];
04-創(chuàng)建渲染緩沖區(qū)
//創(chuàng)建渲染緩存
glGenRenderbuffers(1, colorBufferHandle);
glBindRenderbuffer(GL_RENDERBUFFER, *colorBufferHandle);
// 把渲染緩存綁定到渲染圖層上CAEAGLLayer,并為它分配一個(gè)共享內(nèi)存。
// 并且會(huì)設(shè)置渲染緩存的格式,和寬度
[context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];
05-創(chuàng)建幀緩沖區(qū)
//創(chuàng)建幀緩存
glGenFramebuffers(1, frameBufferHandle);
glBindFramebuffer(GL_FRAMEBUFFER, *frameBufferHandle);
// 把顏色渲染緩存 添加到 幀緩存的GL_COLOR_ATTACHMENT0上,就會(huì)自動(dòng)把渲染緩存的內(nèi)容填充到幀緩存,在由幀緩存渲染到屏幕
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, *colorBufferHandle);
06-創(chuàng)建著色器
什么是著色器?
通常用來(lái)處理紋理對(duì)象,并且把處理好的紋理對(duì)象渲染到幀緩存上,從而顯示到屏幕上。
提取紋理信息,可以處理頂點(diǎn)坐標(biāo)空間轉(zhuǎn)換,紋理色彩度調(diào)整(濾鏡效果)等操作。著色器分為頂點(diǎn)著色器,片段著色器
頂點(diǎn)著色器用來(lái)確定圖形形狀。
片段著色器用來(lái)確定圖形渲染顏色。步驟: 1.編輯著色器代碼 2.創(chuàng)建著色器 3.編譯著色器
只要?jiǎng)?chuàng)建一次,可以在一開(kāi)始的時(shí)候創(chuàng)建編輯著色器代碼 :glsl語(yǔ)言,應(yīng)該屬于gpu編程,如下:
//頂點(diǎn)著色器代碼
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);
}
- 創(chuàng)建 +編譯:屬于動(dòng)態(tài)編譯
GLint status;
//sourceString 是頂點(diǎn)or片段著色器 的代碼文本
const GLchar *source;
source = (GLchar *)[sourceString UTF8String];
//type 頂點(diǎn)or片段著色器
*shader = glCreateShader(type); // 創(chuàng)建著色器
glShaderSource(*shader, 1, &source, NULL);//加載著色器源代碼
glCompileShader(*shader); //編譯著色器
glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);//獲取完成狀態(tài)
if (status == 0) {
// 沒(méi)有完成就直接刪除著色器
glDeleteShader(*shader);
return NO;
}
07-創(chuàng)建著色器程序
//創(chuàng)建一個(gè)著色器程序?qū)ο? GLuint program = glCreateProgram();
_rgbProgram = program;
//關(guān)聯(lián)著色器對(duì)象到著色器程序?qū)ο? //綁定頂點(diǎn)著色器
glAttachShader(program, vertShader);
//綁定片元著色器
glAttachShader(program, fragShader);
// 綁定著色器屬性,方便以后獲取,以后根據(jù)角標(biāo)獲取
// 一定要在鏈接程序之前綁定屬性,否則拿不到
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;
}
/// 獲取全局參數(shù),注意 一定要在連接完成后才行,否則拿不到
_displayInputTextureUniform = glGetUniformLocation(program, "Texture");
//釋放已經(jīng)使用完畢的verShader\fragShader
if (vertShader) {
glDetachShader(program, vertShader);
glDeleteShader(vertShader);
}
if (fragShader) {
glDetachShader(program, fragShader);
glDeleteShader(fragShader);
}
//啟動(dòng)程序
glUseProgram(rgbProgram);
08-創(chuàng)建紋理對(duì)象
通過(guò)獲取的一張一張的圖片(CVPixelBufferRef pixelBuffer),可以把圖片轉(zhuǎn)換為OpenGL中的紋理, 然后再把紋理畫到OpenGL的上下文中
什么是紋理?一個(gè)紋理其實(shí)就是一幅圖像。
紋理映射,我們可以把這幅圖像的整體或部分貼到我們先前用頂點(diǎn)勾畫出的物體上去.
比如繪制一面磚墻,就可以用一幅真實(shí)的磚墻圖像或照片作為紋理貼到一個(gè)矩形上,這樣,一面逼真的磚墻就畫好了。如果不用紋理映射的方法,則墻上的每一塊磚都必須作為一個(gè)獨(dú)立的多邊形來(lái)畫。另外,紋理映射能夠保證在變換多邊形時(shí),多邊形上的紋理圖案也隨之變化。
紋理映射是一個(gè)相當(dāng)復(fù)雜的過(guò)程,基本步驟如下:
1)激活紋理單元、2)創(chuàng)建紋理 、3)綁定紋理 、4)設(shè)置濾波
注意:紋理映射只能在RGBA方式下執(zhí)行
// 創(chuàng)建亮度紋理
// 激活紋理單元0, 不激活,創(chuàng)建紋理會(huì)失敗
glActiveTexture(GL_TEXTURE0);
// 創(chuàng)建紋理對(duì)象
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;
}
//獲取紋理對(duì)象 CVOpenGLESTextureGetName(renderTexture)
//綁定紋理
glBindTexture(CVOpenGLESTextureGetTarget(renderTexture), CVOpenGLESTextureGetName(renderTexture));
//設(shè)置紋理濾波
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格式則需要轉(zhuǎn)為grb格式,一般直播推流時(shí)使用的是yuv格式,此處我是播放的靜態(tài)mp4文件,所以略過(guò)格式轉(zhuǎn)換
// 在創(chuàng)建紋理之前,有激活過(guò)紋理單元,glActiveTexture(GL_TEXTURE0)
// 指定著色器中亮度紋理對(duì)應(yīng)哪一層紋理單元
// 這樣就會(huì)把亮度紋理,往著色器上貼
glUniform1i(displayInputTextureUniform, 0);
if (self.pixelbufferWidth != frameWidth || self.pixelbufferHeight != frameHeight) {
CGSize normalizedSamplingSize = CGSizeMake(1.0, 1.0);
self.pixelbufferWidth = frameWidth;
self.pixelbufferHeight = frameHeight;
/*
//紋理坐標(biāo)
GLfloat quadTextureData[] = {
0.5f, 1.0f,
0.5f, 0.0f,
1.0f, 1.0f,
1.0f, 0.0f,
};
//頂點(diǎn)坐標(biāo)
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;
}
//激活A(yù)TTRIB_VERTEX頂點(diǎn)數(shù)組
glEnableVertexAttribArray(ATTRIB_VERTEX);
//給ATTRIB_VERTEX頂點(diǎn)數(shù)組賦值
glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, quadVertexData);
//激活A(yù)TTRIB_TEXCOORD頂點(diǎn)數(shù)組
glEnableVertexAttribArray(ATTRIB_TEXCOORD);
//給ATTRIB_TEXCOORD頂點(diǎn)數(shù)組賦值
glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, 0, 0, quadTextureData);
//渲染紋理數(shù)據(jù)
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
10-渲染緩沖區(qū)到屏幕
- (void)displayFramebuffer:(CMSampleBufferRef)sampleBuffer{
// 因?yàn)槭嵌嗑€程,每一個(gè)線程都有一個(gè)上下文,只要在一個(gè)上下文繪制就好,設(shè)置線程的上下文為我們自己的上下文,就能繪制在一起了,否則會(huì)黑屏.
if ([EAGLContext currentContext] != _context) {
[EAGLContext setCurrentContext:_context];
}
// 清空之前的紋理,要不然每次都創(chuàng)建新的紋理,耗費(fèi)資源,造成界面卡頓
[self cleanUpTextures];
//執(zhí)行第8步 創(chuàng)建紋理對(duì)象
//設(shè)置視口大小
glViewport(0, 0, backingWidth, backingHeight);
//設(shè)置一個(gè)RGB顏色和透明度,接下來(lái)會(huì)用這個(gè)顏色涂滿全屏
glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
//清除顏色緩沖區(qū)
glClear(GL_COLOR_BUFFER_BIT);
//執(zhí)行第9步 繪制紋理,也就是著色器提取并處理紋理
/// 把上下文的東西渲染到屏幕上
if ([EAGLContext currentContext] == context) {
[context presentRenderbuffer:GL_RENDERBUFFER];
}
}
清理內(nèi)存
- (void)cleanUpTextures {
if (_renderTexture) {
CFRelease(_renderTexture);
_renderTexture = NULL;
}
// 清空紋理緩存
CVOpenGLESTextureCacheFlush(_videoTextureCache, 0);
}
- (void)dealloc {
[self cleanUpTextures];
if(_videoTextureCache) {
CFRelease(_videoTextureCache);
}
}
