前言
前面学习了opengl es渲染管线,可编程语言GLSL,常用的opengl es函数,有了这些基础,现在就可以实现如何将一张图片渲染到屏幕上了。图片,最终在内存中的表现形式就是由一个个像素组成的,而每个像素又由RGB组成,其实opengl es最终渲染的像素是由RGBA四个通道组成的。
opengl es系列文章
opengl es之-基础概念(一)
opengl es之-GLSL语言(二)
opengl es之-GLSL语言(三)
opengl es之-常用函数介绍(四)
opengl es之-渲染两张图片(五)
opengl es之-在图片上添加对角线(六)
opengl es之-离屏渲染简介(七)
opengl es之-CVOpenGLESTextureCache介绍(八)
opengl es之-播放YUV文件(九)
需求
渲染两张图片,并将它们混合,最终呈现到屏幕上
准备
引入头文件,在ios平台,上下文和窗口管理的是由EAGL实现的,这里例子中的opengl es版本为2.0
#import <OpenGLES/ES2/gl.h>
#import <OpenGLES/EAGL.h>
创建上下文环境
基于UIView创建上下文环境,首先要重写class方法,返回return [CAEAGLLayer class];
// 必不可少,否则无法成功创建opengl es环境
+ (Class)layerClass
{
return [CAEAGLLayer class];
}
1、首先设置当前UIView的layer的属性,这里要注意opaque最好为NO,这样可以降低性能消耗。
2、contentsScale系统默认为1.0,这里最好设置为[UIScreen mainScreen].scale,否则由EAGL创建的上下文窗口真正的像素大小就是ios中基于point单位的大小了,这样很明显会造成最终渲染的图片压缩
3、drawableProperties属性按照下面的例子来进行即可,没什么好讲的
- (void)setupContext
{
CAEAGLLayer calayer = (CAEAGLLayer*)self.layer;
calayer.opaque = NO; //CALayer默认是透明的,透明的对性能负荷大,故将其关闭
// 表示屏幕的scale,默认为1;会影响后面renderbufferStorage创建的renderbuffer的长宽值;
// 它的长宽值等于=layer所在视图的逻辑长宽*contentsScale
// 最好这样设置,否则后面按照纹理的实际像素渲染,会造成图片被放大。
calayer.contentsScale = [UIScreen mainScreen].scale;
calayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
// 由应用层来进行内存管理
@(NO),kEAGLDrawablePropertyRetainedBacking,
kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat,
nil];
// 创建指定OpenGL ES的版本的上下文,一般选择2.0的版本
_context = [[EAGLContext alloc] initWithAPI:version];
// 当为yes的时候,所有关于Opengl渲染,指令真正执行都在另外的线程中。NO,则关于渲染,指令真正执行在当前调用的线程
// 对于多核设备有大的性能提升
_context.multiThreaded = yesOrnot;
_memoryPool = CMMemoryPoolCreate(NULL);
// 收到内存不足警告后,需要清除部分内存
__unsafe_unretained __typeof__ (self) weakSelf = self;
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification
object:nil
queue:nil
usingBlock:^(NSNotification *notification) {
__typeof__ (self) strongSelf = weakSelf;
if (strongSelf) {
CVOpenGLESTextureCacheFlush([strongSelf coreVideoTextureCache], 0);
}
}];
}
备注:这里特别要说明的就是上面代码中有一个CMMemoryPoolCreate()创建的内存池,该函数来自于<CoreMedia/CoreMedia.h>框架,其实正常渲染一张图片不需要这个东西,对于渲染视频才需要这个内存池(可以避免重复创建内存),后面在说这个,这里可以先略过
创建帧缓冲区和渲染缓冲区
先说两个概念,帧缓冲区和渲染缓冲区
帧缓冲区:在opengl es中也成为FBO,即用于渲染物体(比如渲染一个白色的正方形,比如将一张图片渲染出来)的一块内存(可能是GPU或者内存条中内存)
渲染缓冲区:在opengl es中,其实将物体渲染到帧缓冲区整个渲染过程就已经结束了,渲染缓冲区是为了将物体再呈现到屏幕上时的一个过渡缓冲区,系统要将物体渲染到屏幕上会先将帧缓冲区中内容拷贝到渲染缓冲区,然后在呈现到屏幕上
具体代码如下
创建帧缓冲区
// 创建frameBuffer
glGenFramebuffers(1, &_frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
创建渲染缓冲区
// 创建渲染buffer
glGenRenderbuffers(1, &_renderBuffer); //第一个参数 创建buffer的数量 第二个 参数 创建的bufferId(为0表示创建失败)
// 绑定刚刚创建的buffer为GL_RENDERBUFFER类型。
glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer); // 第一个参数,buffer的类型,要与前面创建的buffer类型对应。第二个参数,前面创建的buffer id
将渲染缓冲区关联到帧缓冲区,并且由EAGL分配上下文
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _renderBuffer);
// 为render buffer 开辟存储空间 重要;EAGLContext必须设置正确,否则下面会出现36054错误
[_context.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer*)self.layer];
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_renderWidth);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_renderHeight);
glFramebufferRenderbuffer()将渲染缓冲区和帧缓冲区关联了起来,这样渲染结果才能冲这个指定的帧缓冲区拷贝到渲染缓冲区中(此过程由系统完成);renderbufferStorage则是为渲染缓冲区分配了内存,可以看到,其实由glGen glGenRenderbuffers()和glGenFramebuffers()函数并没有真正创建内存,它只是生产了一个可以用来指向这个内存的句柄(也可以成为内存地址)
加载着色器程序
着色器程序由顶点着色器程序和片元着色器程序组成,顶点着色器程序用于渲染管线中的顶点确定阶段,片元着色器程序则用于片元处理阶段。完成一个着色器程序需要经过编译GLSL源代码,链接顶点着色器和片元着色器,执行着色器程序三个步骤。
1、编写GLSL代码
这里的顶点着色器和片元着色器代码如下:
顶点着色器;这里定义了position变量,它表示最终要渲染的图元的顶点的坐标; texcoord表示要加载的图片的纹理的坐标
attribute vec4 position;
attribute vec2 texcoord;
varying highp vec2 v_texcoord;
void main()
{
gl_Position = position;
v_texcoord = texcoord.xy;
}
片元着色器
这个片元着色器的含义就是,将两张图片混合
uniform sampler2D inputImageTexture1;
uniform sampler2D inputImageTexture2;
varying highp vec2 v_texcoord;
void main() {
gl_FragColor = texture2D(inputImageTexture1, v_texcoord)+texture2D(inputImageTexture2, v_texcoord);
}
2、编译GLSL源代码(顶点着色器和片元着色器方式一样)
- (BOOL)compileShader:(GLenum)type sString:(NSString*)sString shader:(GLuint*)shaderRet
{
if (sString.length == 0) {
NSLog(@"着色器程序不能为nil");
return NO;
}
const GLchar *sources = (GLchar*)[sString UTF8String];
// 创建着色器程序句柄
GLuint shader = glCreateShader(type);
if (shader == 0 || shader == GL_INVALID_ENUM) {
NSLog(@"glCreateShader fail");
return NO;
}
// 为着色器句柄添加GLSL代码;可一次添加多个代码,一般添加一个,若添加一个源代码则最后一个参数为NULL 即可
glShaderSource(shader, 1, &sources, NULL);
// 编译该GLSL代码
glCompileShader(shader);
// 打印出编译过程中产生的GLSL的日志
#ifdef DEBUG
GLint logLenght;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLenght);
if (logLenght > 0) {
GLchar *log = (GLchar*)malloc(logLenght);
glGetShaderInfoLog(shader, logLenght, &logLenght, log);
NSLog(@"Shader compile log:\n%s", log);
free(log);
}
#endif
// 检查编译结果
GLint status;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE) {
NSLog(@"compile fail %d",status);
return NO;
}
*shaderRet = shader;
return YES;
}
可以看到大致分为这几个步骤
a、由glCreateShader(type)函数创建着色器句柄,type有两种类型:GL_VERTEX_SHADER(顶点着色器)和GL_FRAGMENT_SHADER(片元着色器),他们分别是opengl es的常量
b、glShaderSource(shader, 1, &sources, NULL);将源程序载入到上面创建的着色器句柄中
c、glCompileShader(shader);编译程序
d、【可选】,可以打印编译过程中的日志,测试阶段用来调试GLSL代码的正确性
e、glGetShaderiv(shader, GL_COMPILE_STATUS, &status);检查编译是否正确
3、生成最终的着色器程序
// 创建一个最终程序句柄;它由顶点着色器和片段着色器组成
filterProgram = glCreateProgram();
// 分别添加顶点着色器程序和片段着色器程序
glAttachShader(filterProgram, vShader);
glAttachShader(filterProgram, fShader);
/** 连接成一个最终程序
* 当这一步完成之后,app就可以和opengl es进行交互了,
* 1、比如app获取glsl中的顶点变量,设置几何图元的顶点以确定图元的最终形状
* 2、app获取glsl中的纹理变量,然后将本地图片传递给这个纹理变量以实现将图片传递给显卡进行渲染和其
* 它处理
*/
glLinkProgram(filterProgram);
// 输出连接过程中的日志
GLint status;
glValidateProgram(filterProgram);
#ifdef DEBUG
GLint logLength;
glGetProgramiv(filterProgram, GL_INFO_LOG_LENGTH, &logLength);
if (logLength > 0){
GLchar *log = (GLchar *)malloc(logLength);
glGetProgramInfoLog(filterProgram, logLength, &logLength, log);
NSLog(@"Program validate log:\n%s", log);
free(log);
}
#endif
// 检查连接结果
glGetProgramiv(filterProgram, GL_LINK_STATUS, &status);
if (status == GL_FALSE) {
NSLog(@"link program fail %d",status);
return nil;
}
由如下几个步骤完成
a、filterProgram = glCreateProgram();创建最终着色器程序句柄
b、glAttachShader(filterProgram, vShader);加载顶点着色器和片元着色器到上面的句柄中
c、glLinkProgram(filterProgram);执行链接
d、【可选】打印链接过程中日志,测试阶段使用
e、glGetProgramiv(filterProgram, GL_INFO_LOG_LENGTH, &logLength);检查连接是否成功
4、执行着色器程序
着色器程序必须得执行才能最终使用
glUseProgram(filterProgram);
渲染图片到帧缓冲区
开辟渲染缓冲区域
glClearColor(0, 1, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
glViewport(0, 0, _renderWidth, _renderHeight);
int w1,h1;
int w2,h2;
void *image1 = [CGImageUtils rgbaImageDataForPath:path1 width:&w1 height:&h1];
NSLog(@"图片的宽和高 w1 %d h1 %d",w1,h1);
void *image2 = [CGImageUtils rgbaImageDataForPath:path2 width:&w2 height:&h2];
NSLog(@"图片的宽和高 w2 %d h2 %d",w2,h2);
1、glViewport(0, 0, _renderWidth, _renderHeight);这个函数很重要,在渲染到帧缓冲区之前,必须开辟一块渲染区域,否则无法渲染
2、rgbaImageDataForPath是我自己封装的一个方法,具体参考Demo,将一张图片转换成RGBA像素数据
给着色器程序传顶点和图片
[self.blendTwoTextureProgram use];
GLuint position = [self.blendTwoTextureProgram attribLocationForName:@"position"];
GLuint texcoord = [self.blendTwoTextureProgram attribLocationForName:@"texcoord"];
GLuint s_texture1 = [self.blendTwoTextureProgram uniformLocationForName:@"inputImageTexture1"];
GLuint s_texture2 = [self.blendTwoTextureProgram uniformLocationForName:@"inputImageTexture2"];
glVertexAttribPointer(position, 2, GL_FLOAT, GL_FALSE, 0, verData1);
glEnableVertexAttribArray(position);
glVertexAttribPointer(texcoord, 2, GL_FLOAT, GL_FALSE, 0, uvData);
glEnableVertexAttribArray(texcoord);
GLint active,textureSize;
glGetIntegerv(GL_ACTIVE_TEXTURE, &active);
NSLog(@"该设备支持的最大纹理单元数目 %d",active);
// 超过此大小则必须先压缩再传给opengl es,否则opengl es无法渲染
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &textureSize);
NSLog(@"该设备支持的纹理最大的长或宽 %d",textureSize);
glActiveTexture(GL_TEXTURE1);
glGenTextures(1,&texturexId2);
glBindTexture(GL_TEXTURE_2D, texturexId2);
// 此方法必须有,第二个参数要与前面激活的纹理单元数对应,前面是GL_TEXTURE1,这里就是1
glUniform1i(s_texture2, 1);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // S方向上的贴图模式
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // T方向上的贴图模式
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, (int)w2, (int)h2, 0, GL_RGBA, GL_UNSIGNED_BYTE, image2);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
/** 纹理的工作原理
* 纹理单元用来将app中图片像素数据传递给OpenGL ES,对应片段着色器中的uniform sampler2D变量。它是一个全局对象,每台设备都会创建数个独立的纹理单元,要使用
* 一个纹理单元之前必须先激活它才能使用,激活之后还需要绑定,则才可以使用多个纹理对象;
* 如果应用中只有一个激活的纹理,则不需要调用glBindTexture()函数也可以,但是如果要使用多个纹理,则必须要调用glBindTexture()进行区分,否则会造成数据被覆盖
*/
glActiveTexture(GL_TEXTURE0);
glGenTextures(1,&texturexId1);
NSLog(@"texturexId %d",texturexId1);
glBindTexture(GL_TEXTURE_2D, texturexId1);
// 此方法必须有,第二个参数要与前面激活的纹理单元数对应,前面是GL_TEXTURE0,这里就是0
glUniform1i(s_texture1, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // S方向上的贴图模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // T方向上的贴图模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 1字节对齐,效率比较低,默认是4;必须在glTexImage2D前面设置
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
// 因为image1像素数据本身就是由RGBA构成的,所以这里format只能是GL_RGBA,如果传其它值则会造成数据不对称而出错
// 比如进行视频渲染是,分别传递YUV三个分量的像素给Opengl es,则那个时候format取值就必须为GL_LUMINANCE或者GL_ALPHA等只有一个字节的
// 最后一个参数可以为NULL,只是分配一块w1xh1的内存空间,当为NULL时,如果前后调用了CGContextDrawImage,这个函
// 数的绘制结果会传给glTexImage2D()开辟的内存空间
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w1, h1, 0, GL_RGBA, GL_UNSIGNED_BYTE, image1); // border参数为1 渲染不出来
// 释放图片像素数据
if (image1) {
free(image1);
}
if (image2) {
free(image2);
}
1、blendTwoTextureProgram是定义的着色器变量对象,通过它可以获取着色器程序中顶点变量(即前面attribute vec4 position;)、纹理坐标变量(attribute vec2 texcoord;)、纹理单元变量(uniform sampler2D inputImageTexture1;)的地址
glVertexAttribPointer()函数用于给顶点变量和纹理坐标变量传值,这里verData1为:
static float verData1[8] = {
-1.0f,-1.0f,// 左下角
1.0f,-1.0f, // 右下角
-1.0f,1.0f, // 左上角
1.0f,1.0f, // 右上角
};
static float uvData[8] = {
0.0f, 1.0f, // 左下角
1.0f, 1.0f, // 右下角
0.0f, 0.0f, // 左上角
1.0f, 0.0f, // 右上角
};
2、glActiveTexture(GL_TEXTURE1);激活纹理单元1,表示使用纹理单元1来处理第二张图
glGenTextures(1,&texturexId2);生成一个纹理句柄
glBindTexture(GL_TEXTURE_2D, texturexId2);将纹理句柄绑定到纹理单元1的GL_TEXTURE_2D类型纹理对象上,那么后续通过glTexxxx()函数设置的参数都是针对这个纹理句柄设置的
glUniform1i()指定前面片元着色中变量uniform sampler2D inputImageTexture1;和uniform sampler2D inputImageTexture2;分别使用哪个纹理单元(在opengl es一般都有数百个纹理单元)来处理图片,
glTexImage2D()用来给着色器传图片
开始渲染
// 确定要绘制的几何图形,该指令执行后才opengl es指令才开始真正的执行;处于渲染管线的第一阶段,每次glDrawArrays()的调用
// 代表前面所有的指令是一次完整的渲染
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
将渲染结果呈现到屏幕上
// 即光栅化阶段,它与opengl es的渲染是独立的,不相干的。当它会将前面所有的渲染结果呈现到屏幕上;
[_context.context presentRenderbuffer:GL_RENDERBUFFER];
项目地址
具体可参考项目中GLView的方法,里面有很详细的注释
// 一次渲染多张图片;同时渲染两张张图片,效果是混合这两张图片
- (void)renderTextureWithPath:(NSString*)path path2:(NSString*)path2;