OpenGLES(五)- ESLS案例:纹理贴图
阅读时间大概10-15分钟
结果效果图
首先梳理一下大体思路,下方代码也会按照这个顺序:
代码语言:javascript复制1. EAGLLayer获取,设置layer图层
2. content创建
3. 清空缓存区(frameBuffer,renderBuffer)
4. 设置renderBuffer
5. 设置frameBuffer
6. 手动编译、链接着色器程序
7. 开始绘制 - 着色器创建
全局属性定义
代码语言:javascript复制//变量名基本就是解释
@property(nonatomic,strong)CAEAGLLayer *myLayer;
@property(nonatomic,strong)EAGLContext *myContent;
@property(nonatomic,assign)GLuint myRenderBuffer;
@property(nonatomic,assign)GLuint myFrameBuffer;
@property(nonatomic,assign)GLuint myProgram;
1. EAGLLayer获取,设置layer图层
代码语言:javascript复制 (Class)layerClass{
//1.需要重写view的子类方法,返回特定的layer,否则所有绘制动作是无效的
return [CAEAGLLayer class];
}
-(void)setupLayout {
//2.获取layer
//view中存在一个特殊的图层,用于OpenGL的渲染
self.myLayer = (CAEAGLLayer *)self.layer;
//3.设置scale
CGFloat scale = [[UIScreen mainScreen] scale];
[self setContentScaleFactor:scale];
//4.设置描述属性
/*
kEAGLDrawablePropertyRetainedBacking 表示绘图表面显示后,是否保留其内容。
kEAGLDrawablePropertyColorFormat 可绘制表面的内部颜色缓存区格式。这个key对应的值是一个NSString指定特定颜色缓存区对象。默认是kEAGLColorFormatRGBA8;
*/
self.myLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@false,
kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8};
}
- 需要注意的是
layerClass
这个方法的重写
2. content创建
代码语言:javascript复制-(void)setupContent{
//1. 创建上下文
self.myContent = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
if(!self.myContent){
NSLog(@"create content failed");
return;
}
//2.设置图形上下文
if(![EAGLContext setCurrentContext:self.myContent]){
NSLog(@"set Current Context failed");
return;
}
}
3.清空缓存区
代码语言:javascript复制-(void)cleanBuffer {
/*
buffer分为frame buffer 和 render buffer2个大类。
其中frame buffer 相当于render buffer的管理者。
frame buffer object即称FBO。
render buffer则又可分为3类。colorBuffer、depthBuffer、stencilBuffer。
*/
//1. 清空渲染缓存区
//该渲染缓存区被重置为0,被标记为未使用。与之连接的帧缓存区也被断开。
glDeleteRenderbuffers(1, &_myRenderBuffer);
//2. 清空帧缓存区
//使用该函数和glDeleteFramebuffers效果相同,但是renderBuffer也可以使用
glDeleteBuffers(1, &_myFrameBuffer);
}
在设置之前需要解释一下FrameBuffer和RenderBuffer
FrameBuffer是RenderBuffer的管理者
,两者共同组成了帧缓存区。FrameBuffer是没有存储功能的,具体的存储功能实际是RenderBuffer。
图片来自简书-Style_月月
- FrameBuffer上有3个附着点:
颜色附着点(Color Attachment):管理纹理、颜色缓冲区
深度附着点(depth Attachment):会影响颜色缓冲区,管理深度缓冲区(Depth Buffer)
模板附着点(Stencil Attachment):管理模板缓冲区(Stencil Buffer)
- RenderBuffer有3种缓存区
深度缓存区(Depth Buffer):存储深度值等
模板缓存区(Stencil Buffer):存储模板
- 纹理缓存区( Texture mip Images)
保存的是MipMap中当前深度的切片。所以需要深度附着点和颜色附着点共同协作。
4. 设置renderBuffer
代码语言:javascript复制-(void)setRenderBuffer{
//1. 创建渲染缓冲区ID
GLuint rBuffer;
glGenRenderbuffers(1, &rBuffer);
//2. 绑定缓冲区
glBindRenderbuffer(GL_RENDERBUFFER, rBuffer);
//3. 指定存储在 renderbuffer 中图像的宽高以及颜色格式(从myLayer中获取),并按照此规格为之分配存储空间
[self.myContent renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myLayer];
self.myRenderBuffer = rBuffer;
}
5. 设置frameBuffer(FBO)
代码语言:javascript复制//1. 创建渲染缓冲区ID
GLuint fBuffer;
glGenFramebuffers(1, &fBuffer);
//2. 绑定缓冲区
glBindFramebuffer(GL_FRAMEBUFFER, fBuffer);
/*3. 生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定,
使用函数进将渲染缓存区绑定到d帧缓存区对应的颜色附着点上,后面的绘制才能起作用
*/
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myRenderBuffer);
6.着色器创建
- 手动编译着色器代码量还是有点多的但是思路依旧很清晰
手动编译、链接着色器程序:
- 顶点、片元着色器ID创建
- 着色器文件读取
- 着色器文件附着到着色器上
- 着色器编译
- 程序ID创建
- 着色器附着到程序上
- 清理着色器内存
- 程序链接 8.1 链接状态获取
- 使用program
-(void)setupShader{
//1. 读取着色器地址
NSString *verFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
NSString *framFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
//2. 加载、编译着色器,编辑、链接程序对象
self.myProgram = [self startShaderProgram:verFile fFile:framFile];
}
/// 着色器程序启动
-(GLuint)startShaderProgram:(NSString *)vertex fFile:(NSString *)fragment{
//定义2个零时着色器对象
GLuint verSharder, fragSharder;
//着色器编译
[self compileShader:&verSharder type:GL_VERTEX_SHADER path:vertex];
[self compileShader:&fragSharder type:GL_FRAGMENT_SHADER path:fragment];
//程序编译
GLuint program;
program = [self compileProgram:verSharder frag:fragSharder];
//程序链接
[self linkProgram:program];
return program;
}
/// 顶点着色器创建、编译
-(void)compileShader:(GLuint *)shader type:(GLenum)type path:(NSString *)path{
//1. 顶点、片元着色器ID创建
*shader = glCreateShader(type);
//2. 读取着色器文件
NSString *source = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
//转为c字符串
const GLchar* cSource = [source UTF8String];
//3.将着色器源码附加到着色器对象上。
//参数1:shader,要编译的着色器对象 *shader
//参数2:numOfStrings,传递的源码字符串数量 1个
//参数3:strings,着色器程序的源码(真正的着色器程序源码)
//参数4:lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
glShaderSource(*shader, 1, &cSource, NULL);
//4.着色器编译
glCompileShader(*shader);
}
/// 程序对象创建
-(GLuint)compileProgram:(GLuint)vertexShader frag:(GLuint)fragShader {
//5. 程序ID创建
GLint program = glCreateProgram();
//6. 着色器附着到程序上,创建最终的程序
glAttachShader(program, vertexShader);
glAttachShader(program, fragShader);
//7. 不会立即删除着色器,而是将着色器进行标记,等待着色器不在连接任何程序对象时,他的内存将会被释放。
glDeleteShader(vertexShader);
glDeleteShader(fragShader);
return program;
}
/// 程序链接
-(void)linkProgram:(GLuint)program {
//8. 程序链接
glLinkProgram(program);
GLint linkStatus;
//获取编译状态
glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
if (linkStatus == GL_FALSE) {
GLchar info[512];
glGetProgramInfoLog(program, sizeof(info), 0, &info[0]);
NSString *message = [NSString stringWithUTF8String: info];
NSLog(@"Program Link Error:%@",message);
return;
}
NSLog(@"Program Link Success!");
//9.使用program
glUseProgram(program);
}
7.开始绘制
代码语言:javascript复制-(void)setupTexture{
//1. 设置清屏颜色,颜色缓存区
glClearColor(0.3, 0.2, 0.7, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
//2.设置视口
CGFloat scale = [[UIScreen mainScreen] scale];
glViewport(self.frame.origin.x * scale,
self.frame.origin.y * scale,
self.frame.size.width * scale,
self.frame.size.height * scale);
//3.设置顶点、纹理坐标
//前3个是顶点坐标,后2个是纹理坐标
GLfloat attrArr[] =
{
0.5f, -0.5f, -1.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, -1.0f, 0.0f, 1.0f,
0.5f, -0.5f, -1.0f, 1.0f, 0.0f,
};
//4.-----创建顶点缓存区--------
//4.1 创建顶点缓存区
GLuint vertex;
glGenBuffers(1, &vertex);
//4.2 绑定顶点缓存区
glBindBuffer(GL_ARRAY_BUFFER, vertex);
//4.3 将数据从内存中读取到顶点缓存区中
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_STATIC_DRAW);
//5.-----处理顶点数据--------
//5.1 获取顶点着色器中限定符为:attribute的句柄
//注意:第二参数字符串必须和顶点着色器中的输入变量名保持一致
GLuint position = glGetAttribLocation(self.myProgram, "position");
//5.2 允许该变量position读取顶点缓存区的数据
glEnableVertexAttribArray(position);
//5.3 设置positions通过何种方式从顶点缓存区中读取顶点数据
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (float *)NULL 0);
//6.-----处理纹理坐标数据--------
//6.1 获取顶点着色器中限定符为:attribute的句柄
GLuint texCoord = glGetAttribLocation(self.myProgram, "textureCoord");
//6.2 允许该变量texCoord读取顶点缓存区的数据
glEnableVertexAttribArray(texCoord);
//6.3 设置texCoord通过何种方式从顶点缓存区中读取纹理数据
glVertexAttribPointer(texCoord, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (float *)NULL 3);
//7. 加载纹理图片
[self loadImage:@"cat"];
//8.-----处理纹理数据--------
//8.1 获取着色器中限定符为:uniform的句柄
GLuint texture = glGetUniformLocation(self.myProgram, "textureMap");
//8.2 设置texture读取帧缓存区中的对应纹理ID=0(参数2)的纹理
glUniform1f(texture, 0);
//9. 绘制
glDrawArrays(GL_TRIANGLES, 0, 6);
//10. 从渲染缓存区显示到屏幕上
[self.myContent presentRenderbuffer:GL_RENDERBUFFER];
}
//加载纹理图片
-(BOOL)loadImage:(NSString *)picName{
//1.将UIImage转为CGImage
CGImageRef ref = [UIImage imageNamed:picName].CGImage;
//判断图片是否获取成功
if (!ref) {
NSLog(@"Failed to load image %@", picName);
return NO;
}
//2.读取图片大小、颜色空间
size_t width = CGImageGetWidth(ref);
size_t height = CGImageGetHeight(ref);
CGColorSpaceRef space = CGImageGetColorSpace(ref);
//3. 初始化接收图片数据的变量
GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
//4.创建coreGraphics的上下文
/*
参数1:data,指向要渲染的绘制图像的内存地址
参数2:width,bitmap的宽度,单位为像素
参数3:height,bitmap的高度,单位为像素
参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
参数5:bytesPerRow,bitmap的每一行的内存所占的比特数
参数6:colorSpace,bitmap上使用的颜色空间 kCGImageAlphaPremultipliedLast:RGBA
*/
CGContextRef contentRef = CGBitmapContextCreate(spriteData, width, height, 8, width*4, space, kCGImageAlphaPremultipliedLast);
//5. 将CGImage在CGContextRef上绘制出来
/*
CGContextDrawImage 使用的是Core Graphics框架,坐标系与UIKit 不一样。UIKit框架的原点在屏幕的左上角,Core Graphics框架的原点在屏幕的左下角。
CGContextDrawImage
参数1:绘图上下文
参数2:rect坐标
参数3:绘制的图片
*/
CGContextDrawImage(contentRef, CGRectMake(0, 0, width, height), ref);
//6. 绘制完毕后释放CG上下文
CGContextRelease(contentRef);
//以上步骤统称为图片解压缩
//7. 激活纹理空间
//OpenGL中纹理ID0默认打开,所以该方法可省略
//glActiveTexture(0);
//8. 绑定纹理ID
glBindTexture(GL_TEXTURE_2D, 0);
//9. 设置纹理ID的参数
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
//10. 载入纹理到帧缓存区中,并对应纹理ID=0
float fw = width, fh = height;
/*
参数1:纹理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
参数2:加载的层次,一般设置为0
参数3:纹理的颜色值GL_RGBA
参数4:宽
参数5:高
参数6:border,边界宽度
参数7:format
参数8:type
参数9:纹理数据
*/
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
free(spriteData);
return YES;
}
- glVertexAttribPointer参数解释可参考OpenGLES(二)- 纹理贴图
绘制完成后,会发现绘制完成后图片是倒的。因为:顶点坐标的原点是在左下角,而纹理坐标的原点是在左上角。目前有4种思路来解决:
- 在CoreGraphic解压缩图片时,旋转图片(最常使用的方案)
- 在顶点着色器中使用矩阵旋转、缩放变换
- 在顶点、片元着色器中将纹理Y地址进行1-Y的翻转操作
- 修改纹理坐标,使之翻转
OpenGL ES中图片倒置解决方案