OpenGL ES之GLSL渲染图片显示的整体流程
整体思路
本文不采用UIKit的GLKBaseEffect渲染一张图片的显示,而是使用编译链接自定义的着色器(shader)。用GLSL语言来实现自定义顶点/片元着色器,并将图形进行简单的渲染显示。
一、GLKit渲染
- 利用GLKit渲染加载一张jpg/png图片,请参考之前的文章:OpenGL ES之GLKit的使用功能和API说明。
二、GLSL自定义着色器实现图片渲染的思路说明
- 创建图层:重写layerClass,将YDWView返回的图层从CALayer替换成CAEAGLLayer,并设置描述属性;
- 创建上下文:上下文主要是用于保存OpenGL ES中的状态,是一个状态机,不论是GLKIt还是GLSL,都是需要context的;
- 清空缓存区:buffer分为 frameBuffer 和 renderBuffer 两个大类,都需要清空;
- 设置RenderBuffer:渲染缓存区
- 设置FrameBuffer:帧缓存区
- 开始绘制:读取顶点/片元着色器的程序,并加载shader,编译顶点着色程序/片元着色器程序,然后链接程序,设置顶点、纹理坐标,并处理顶点坐标和纹理。
三、GLSL着色语言
- GLSL的基础使用、数据类型和常用API请参考:OpenGL ES之着色语言GLSL的使用说明及API
准备工作
一、ViewController和View
- 新建一个view继承UIView,命名为YDWView;
- 在ViewController中引入#import “YDWView.h”,并声明一个@property(nonatomic, strong)YDWView *myView属性。在viewDidLoad加载myView:
- (void)viewDidLoad {
[super viewDidLoad];
self.myView = (YDWView *)self.view;
}
- 在YDWView中定义部分需要用到的全局变量(在iOS和tvOS上绘制OpenGL ES内容的图层,继承CALayer),并重载layoutSubviews和重写layerClass方法(重写layerClass,将YDWView返回的图层从CALayer替换成CAEAGLLayer):
@property (nonatomic, strong) CAEAGLLayer *myEagLayer;
@property (nonatomic, strong) EAGLContext *myContext;
@property (nonatomic, assign) GLuint myColorRenderBuffer;
@property (nonatomic, assign) GLuint myColorFrameBuffer;
@property (nonatomic, assign) GLuint myPrograme;
- (void)layoutSubviews {
}
// 重写layerClass,将YDWView返回的图层从CALayer替换成CAEAGLLayer
+ (Class)layerClass {
return [CAEAGLLayer class];
}
二、自定义着色器的vsh和fsh文件创建
- 创建shader.vsh文件:
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;
void main() {
varyTextCoord = textCoordinate;
gl_Position = position;
}
- 创建shaderv.fsh文件:
precision highp float;
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;
void main() {
gl_FragColor = texture2D(colorMap, varyTextCoord);
}
渲染流程
一、创建图层
- 创建特殊图层并设置scale
/* 创建特殊图层
* 重写layerClass,将YDWView返回的图层从CALayer替换成CAEAGLLayer
*/
self.myEagLayer = (CAEAGLLayer *)self.layer;
// 设置scale
[self setContentScaleFactor:[[UIScreen mainScreen] scale]];
- 设置描述属性,不维持渲染内容以及颜色格式为RGBA8;
① kEAGLDrawablePropertyRetainedBacking: 表示绘图表面显示后,是否保留其内容;
② kEAGLDrawablePropertyColorFormat :表示可绘制表面的内部颜色缓存区格式,这个key对应的值是一个NSString指定特定颜色缓存区对象,默认是kEAGLColorFormatRGBA8;
kEAGLDrawablePropertyColorFormat | 说明 |
---|---|
kEAGLColorFormatRGBA8 | 32位RGBA的颜色,4*8=32位 |
kEAGLColorFormatRGB565 | 16位RGB的颜色 |
kEAGLColorFormatSRGBA8 | sRGB代表了标准的红、绿、蓝,即CRT显示器、LCD显示器、投影机、打印机以及其他设备中色彩再现所使用的三个基本色素。sRGB的色彩空间基于独立的色彩坐标,可以使色彩在不同的设备使用传输中对应于同一个色彩坐标体系,而不受这些设备各自具有的不同色彩坐标的影响 |
self.myEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false, kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];
二、创建上下文
- 创建图形上下文,指定OpenGL ES 渲染API版本
// 创建图形上下文
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
- 判断是否创建成功和设置图形上下文是否成功
// 判断是否创建成功
if (!context) {
return;
}
// 判断设置图形上下文是否成功
if (![EAGLContext setCurrentContext:context]) {
return;
}
- 将局部context,变成全局的context
// 将局部context,变成全局的context
self.myContext = context;
三、清空缓存区
-
⼀个 renderbuffer 对象是通过应⽤分配的⼀个2D图像缓存区。renderbuffer 能够被⽤来分配和存储颜⾊、深度或者模板值。也能够在⼀个framebuffer被⽤作颜⾊、深度、模板的附件。⼀个renderbuffer是⼀个类似于屏幕窗⼝系统提供可绘制的表⾯。⽐如pBuffer。⼀个renderbuffer,然后它并不能直接的使⽤像⼀个GL 纹理。
-
⼀个 frameBuffer 对象(通常被称为⼀个FBO)。是⼀个收集颜⾊、深度和模板缓存区的附着点。描述属性的状态,例如颜⾊、深度和模板缓存区的⼤⼩和格式,都关联到FBO(Frame Buffer Object)。并且纹理的名字和renderBuffer 对象也都是关联于FBO。各种各样的2D图形能够被附着framebuffer对象的颜⾊附着点。它们包含了renderbuffer对象存储的颜⾊值、⼀个2D纹理或⽴⽅体贴图。或者⼀个mip-level的⼆维切⾯在3D纹理。同样,各种各样的2D图形包含了当时的深度值可以附加到⼀个FBO的深度附着点钟去。唯⼀的⼆维图像,能够附着在FBO的模板附着点,是⼀个renderbuffer对象存储模板值。
-
buffer分为 frameBuffer 和 renderBuffer 两个大类,其中 frameBuffer 相当于 renderBuffer 的管理者;
-
frame buffer object 即称 FBO;
-
renderBuffer 则又可分为3类:colorBuffer(颜⾊缓存区)、depthBuffer(深度缓存区)、stencilBuffer(模板缓存区)。
glDeleteBuffers(1, &_myColorFrameBuffer);
self.myColorFrameBuffer = 0;
glDeleteBuffers(1, &_myColorRenderBuffer);
self.myColorRenderBuffer = 0;
四、设置RenderBuffer
// 定义一个缓存区ID
GLuint buffer;
// 申请一个缓存区标志
glGenBuffers(1, &buffer);
// 将局部colorBuffer,变成全局的colorBuffer
self.myColorRenderBuffer = buffer;
// 将标识符绑定到GL_RENDERBUFFER
glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
// 将可绘制对象drawable object's CAEAGLLayer的存储绑定到OpenGL ES renderBuffer对象
[self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
五、设置FrameBuffer
// 定义一个缓存区ID
GLuint buffer;
// 申请一个缓存区标志
glGenBuffers(1, &buffer);
// 将局部frameBuffer,变成全局的frameBuffer
self.myColorFrameBuffer = buffer;
// 将标识符绑定到GL_RENDERBUFFER
glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
// 将渲染缓存区myColorRenderBuffer 通过glFramebufferRenderbuffer函数绑定到GL_COLOR_ATTACHMENT0上
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
- 生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定,调用glFramebufferRenderbuffer函数进行绑定到对应的附着点上,绘制才能起作用。
六、开始绘制
- 设置视口大小
// 设置清屏颜色
glClearColor(0.4f, 0.4f, 0.4f, 1.0f);
// 清除颜色缓存区
glClear(GL_COLOR_BUFFER_BIT);
// 设置视口大小
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);
- 读取顶点着色程序、片元着色程序
// 读取顶点着色程序、片元着色程序
NSString *vertFile = [[NSBundle mainBundle] pathForResource:@"shader" ofType:@"vsh"];
NSString *fragFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"fsh"];
// 加载shader
self.myPrograme = [self loadShaders:vertFile Withfrag:fragFile];
- 加载shader
// 加载shader
- (GLuint)loadShaders:(NSString *)vert Withfrag:(NSString *)frag {
// 定义2个临时着色器对象
GLuint verShader, fragShader;
GLint program = glCreateProgram();
/* 编译顶点着色程序、片元着色器程序
* 参数1:编译完存储的底层地址
* 参数2:编译的类型,GL_VERTEX_SHADER(顶点)、GL_FRAGMENT_SHADER(片元)
* 参数3:文件路径
*/
[self compileShader:&verShader type:GL_VERTEX_SHADER file:vert];
[self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
// 创建最终的程序
glAttachShader(program, verShader);
glAttachShader(program, fragShader);
// 释放不需要的shader
glDeleteShader(verShader);
glDeleteShader(fragShader);
return program;
}
- 编译顶点着色程序、片元着色器程序
// 编译顶点着色程序、片元着色器程序
- (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file {
// 读取文件路径字符串
NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
const GLchar* source = (GLchar *)[content UTF8String];
// 创建一个shader(根据type类型)
*shader = glCreateShader(type);
/* 将着色器源码附加到着色器对象上
* 参数1:shader,要编译的着色器对象 *shader
* 参数2:numOfStrings,传递的源码字符串数量 1个
* 参数3:strings,着色器程序的源码(真正的着色器程序源码)
* 参数4:lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
*/
glShaderSource(*shader, 1, &source,NULL);
// 把着色器源代码编译成目标代码
glCompileShader(*shader);
}
- 链接:判断是否链接成功
// 链接
glLinkProgram(self.myPrograme);
GLint linkStatue;
// 获取链接状态
glGetProgramiv(self.myPrograme, GL_LINK_STATUS, &linkStatue);
if (linkStatue == GL_FALSE) {
GLchar message[512];
glGetProgramInfoLog(self.myPrograme, sizeof(message), 0, &message[0]);
NSString *messageString = [NSString stringWithUTF8String:message];
NSLog(@"Program Link Error:%@",messageString);
return;
}
- 使用program
// 使用program
glUseProgram(self.myPrograme);
- 设置顶点、纹理坐标,并处理顶点坐标和纹理
// 设置顶点、纹理坐标,前3个是顶点坐标,后2个是纹理坐标
GLfloat attrArray[] = {
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,
};
/* 处理顶点数据
* 1.顶点缓存区
* 2.申请一个缓存区标识符
* 3.将attrBuffer绑定到GL_ARRAY_BUFFER标识符上
* 4.把顶点数据从CPU内存复制到GPU上
*/
GLuint attrBuffer;
glGenBuffers(1, &attrBuffer);
glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArray), attrArray, GL_DYNAMIC_DRAW);
/* 将顶点数据通过myPrograme中的传递到顶点着色程序的position
* glGetAttribLocation,用来获取vertex attribute的入口的
* 告诉OpenGL ES,通过glEnableVertexAttribArray
* 最后数据是通过glVertexAttribPointer传递过去的
*/
// 第二参数字符串必须和shaderv.vsh中的输入变量:position保持一致
GLuint position = glGetAttribLocation(self.myPrograme, "position");
// 设置合适的格式从buffer里面读取数据
glEnableVertexAttribArray(position);
/* 设置读取方式
* 参数1:index,顶点数据的索引
* 参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
* 参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
* 参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
* 参数5:stride,连续顶点属性之间的偏移量,默认为0;
* 参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
*/
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL);
/* 处理纹理数据
* glGetAttribLocation,用来获取vertex attribute的入口的
* 第二参数字符串必须和shaderv.vsh中的输入变量:textCoordinate保持一致
*/
GLuint textCoor = glGetAttribLocation(self.myPrograme, "textCoordinate");
// 设置合适的格式从buffer里面读取数据
glEnableVertexAttribArray(textCoor);
/* 设置读取方式
* 参数1:index,顶点数据的索引
* 参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
* 参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
* 参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
* 参数5:stride,连续顶点属性之间的偏移量,默认为0
* 参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
*/
glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (float *)NULL + 3);
// 加载纹理
[self setupTexture:@"yiyi"];
- 从图片中加载纹理
// 从图片中加载纹理
- (GLuint)setupTexture:(NSString *)fileName {
// 将 UIImage 转换为 CGImageRef
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
// 判断图片是否获取成功
if (!spriteImage) {
NSLog(@"Failed to load image %@", fileName);
exit(1);
}
// 读取图片的大小,宽和高
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);
// 获取图片字节数 宽*高*4(RGBA)
GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
/* 创建上下文
* 参数1:data,指向要渲染的绘制图像的内存地址
* 参数2:width,bitmap的宽度,单位为像素
* 参数3:height,bitmap的高度,单位为像素
* 参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
* 参数5:bytesPerRow,bitmap的没一行的内存所占的比特数
* 参数6:colorSpace,bitmap上使用的颜色空间 kCGImageAlphaPremultipliedLast:RGBA
*/
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
/* 在CGContextRef上将图片绘制出来
* CGContextDrawImage 使用的是Core Graphics框架,坐标系与 UIKit 不一样
* UIKit框架的原点在屏幕的左上角,Core Graphics框架的原点在屏幕的左下角
* CGContextDrawImage
参数1:绘图上下文
参数2:rect坐标
参数3:绘制的图片
*/
CGRect rect = CGRectMake(0, 0, width, height);
// 使用默认方式绘制
CGContextDrawImage(spriteContext, rect, spriteImage);
// 画图完毕就释放上下文
CGContextRelease(spriteContext);
// 绑定纹理到默认的纹理ID(
glBindTexture(GL_TEXTURE_2D, 0);
/* 设置纹理属性
* 参数1:纹理维度
* 参数2:线性过滤、为s,t坐标设置模式
* 参数3:wrapMode,环绕模式
*/
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);
float fw = width, fh = height;
/* 载入纹理2D数据
* 参数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);
// 释放spriteData
free(spriteData);
return 0;
}
- 渲染绘制
// 设置纹理采样器 sampler2D
glUniform1i(glGetUniformLocation(self.myPrograme, "colorMap"), 0);
// 绘图
glDrawArrays(GL_TRIANGLES, 0, 6);
// 从渲染缓存区显示到屏幕上
[self.myContext presentRenderbuffer:GL_RENDERBUFFER];
- 最后在layoutSubviews调用这些方法即可
- (void)layoutSubviews {
[self setupLayer];
[self setupContext];
[self clearRenderAndFrameBuffer];
[self setupRenderBuffer];
[self setupFrameBuffer];
[self renderLayer];
}
- 到这里,一张图片的渲染显示,就已经完成了。但是大家可能已经发现:渲染出来的图片怎么是倒着的呢?其实这是因为OpenGL要求y轴0.0坐标是在图片的底部的,但是图片的y轴0.0坐标通常在顶部。所以这里提供一种解决方案:在shaderv.fsh文件中将gl_FragColor = texture2D(colorMap, varyTextCoord);改为:gl_FragColor = texture2D(colorMap, vec2(varyTextCoord.x,1.0-varyTextCoord.y));即可。
- 完整代码传送门:OpenGL_ES之GLSL渲染加载图片
上一篇: OpenGL之利用模型视图矩阵和投影矩阵让球体自动旋转
下一篇: vue 面试问题总结