题记:本章会继续介绍OpenGL基础,参考LearnOpenGL CN能学到很多。本章会介绍OpenGL绘图部分,同样结合自己在学习中有疑问地方加以说明。本人音视频学习Demo也有OpenGL在相机、视频方面的应用,文章或代码若有错误,也希望大佬不吝赐教。
一、OpenGL渲染基础
1.1 FBO
关于Frame Buffer Object,在iOS端图像渲染有所介绍。在LearnOpenGL CN中FBO属于高级内容,但其实在iOS中使用CAEAGLLayer就会不可避免用到,同时它也是理解滤镜(美颜等)的基础。
1.1.1 画框
FBO可以理解为OpenGL的画框,或者说可以自定义的画布框架。之所以这么说是因为FBO本身不存储数据,而是管理附件,附件一般用于存储颜色和深度模版等数据。
图中左侧可以类比理解为画布,右侧可以类比为卡槽。画布有两种“材质”,RBO和纹理,卡槽又分为颜色、深度、模板。可以参考前面
1.1.1.1 RBO VS 纹理
-
RBO
渲染缓冲对象(Render Buffer Object, RBO) 是一种用于临时存储渲染数据的缓冲对象。它专门设计用于高效存储 颜色、深度或模板数据,但不支持直接通过着色器采样。
//1.定义一个缓存区 GLuint buffer; //2.申请一个缓存区标志 glGenRenderbuffers(1, &buffer); //3. self.colorRenderBuffer = buffer; //4.将标识符绑定到GL_RENDERBUFFER glBindRenderbuffer(GL_RENDERBUFFER, self.colorRenderBuffer); // 分配深度和模板联合存储(24位深度 + 8位模板) glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); // 绑定 FBO glBindFramebuffer(GL_FRAMEBUFFER, fbo); // 附加为深度和模板附件 glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);iOS中用于渲染CAEAGLLayer时仅需要替换存储语句。
[self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.gpuLayer]; -
纹理
纹理下文中会有更加详细说明,这里介绍的是绑定到FBO。
// 创建纹理 GLuint texture; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); // 创建 FBO 并附加纹理 GLuint fbo; glGenFramebuffers(1, &fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); -
对比
特性 渲染缓冲对象(RBO) 纹理(Texture) 设计目的 临时存储渲染数据(颜色、深度、模板) 存储可复用的图像数据,支持采样和着色器访问 数据访问 只能通过 glReadPixels读取像素数据支持通过纹理采样器( sampler2D)在着色器中直接访问性能优化 适合一次性操作(如深度测试) 适合需要后续复用数据的场景(如后处理) 内存管理 数据存储由 OpenGL 驱动管理,不可直接操作 可通过 glTexImage2D手动分配和更新数据内存占用 较低(驱动优化存储) 较高(显存占用明确) 适用场景 深度/模板测试、临时颜色缓冲 需要重复访问数据的场景(如动态贴图)
RBO绑定到FBO时,只能通过glReadPixels读取像素数据,无法像纹理一样直接采样。若需要将渲染结果作为纹理使用(如滤镜链),选择纹理附件。而RBO更适合临时存储不需要复用的数据(如深度缓冲或者显示)。
1.1.2 离屏渲染
离屏渲染常常被作为iOS的面试内容,但接触了FBO会有更深入的理解。离屏渲染是指GPU将内容绘制到与屏幕显示无关的缓冲区(Offscreen Buffer),而不是直接渲染到当前屏幕的帧缓冲中。
-
离屏渲染应用场景
主动创建离屏FBO用于后处理(如模糊、滤镜)或多阶段渲染,如在音视频学习Demo中,实现如下效果(随意的测试效果)。
使用配置如下,原图读入纹理-》画人脸点-》灰度-》面罩-》iOS CAEAGLLayer显示。在CAEAGLLayer显示的一层FBO绑定的RBO,属于当前屏幕渲染(有些资料上显示非0的FrameBuffer都是离屏渲染是不对的)。而前面的步骤FBO绑定的都是纹理,并且上一步的结果作为下一步的输入,都属于离屏渲染。
/// 这里可以配置滤镜 pointsFilter->addTarget(grayFilter); grayFilter->addTarget(maskFilter); maskLink->input = pointsFilter; maskLink->output = maskFilter; [self.gpuView setFilterLinks:maskLink]; -
离屏渲染误区
iOS面试中经常被问到的是如何避免离屏渲染。离屏渲染需要额外创建缓冲区,而且需要切换上下文,比较耗费性能,所以需要避免自动触发的离屏渲染的场景,如阴影、遮罩、半透明等。
先看一下为何需要自动触发离屏渲染,正常的渲染流程采用油画算法由远及近的渲染图层。但在一些情况下,如下图按钮圆角+图片(
clipsToBounds=YES),或者组透明度(allowsGroupOpacity=YES)。油画算法无法先画完按钮和图片,再在原画布切割成圆角,(可以想象PS时借助图层才能完成)需要先在一个透明图层上处理完这些再渲染到当前FrameBuffer上,于是就是离屏渲染了。关于离屏渲染的场景,可以参考其他材料。这里想说的是一种误解。
-
Core Graphics绘图
- 把绘图任务交给了CPU
- 简单的圆角绘制场景下,可以避免离屏渲染的额外开销,
可能具有更高的性能,最好结合场景测试后再给结论。 - 更合适的做法,使用异步线程处理,处理完再交给主线程。
- (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); CGRect bounds = self.bounds; CGFloat cornerRadius = 10.0; // 创建一个圆角矩形路径 UIBezierPath *roundedPath = [UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius]; CGContextAddPath(context, roundedPath.CGPath); // 裁剪上下文 CGContextClip(context); // 绘制内容 [[UIColor redColor] setFill]; CGContextFillRect(context, bounds); } -
mask设置圆角
- 需要额外framebuffer,会触发离屏渲染
- 设置圆角时不会比自动触发离屏渲染性能好
- 在处理复杂图形时,硬件加速更有优势,会比Core Graphics绘图有更好性能。
// 创建一个遮罩图层 CAShapeLayer *maskLayer = [CAShapeLayer layer]; UIBezierPath *roundedPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds cornerRadius:10.0]; maskLayer.path = roundedPath.CGPath; // 设置遮罩图层 view.layer.mask = maskLayer;- 光栅化(
layer.shouldRasterize=YES)- 系统会把图层内容渲染成一张位图并缓存起来,本身就是离屏渲染
- 如果该图层的内容没有发生变化,系统就可以直接使用缓存中的位图,从而大大提高了渲染效率。
- 如果图层内容频繁变化,每次变化都需要重新进行离屏渲染,带来更多的性能开销。
-
1.2 基本图形
上一篇中介绍了基本流程和坐标系,这一小节说明绘制基本图形。
glDrawArrays(GL_TRIANGLES, 0, 6); // glDrawElements
// 展示缓冲区
[self.context presentRenderbuffer:GL_RENDERBUFFER];
相当于开始落笔绘画了,有下列笔法(移动端OpenGL ES仅支持三角形,OpenGL支持更多图形):
| 类型 | 说明 | 画线 |
|---|---|---|
| GL_POINTS | 绘制独立的点 | |
| GL_LINES | 每2个顶点组成一条线段 | |
| GL_LINE_STRIP | 连续线段 | |
| GL_LINE_LOOP | 闭合连续线段 | |
| GL_TRIANGLES | 每3个顶点组成一个三角形 | |
| GL_TRIANGLE_STRIP | 带状三角形,一条边共享 | |
| GL_TRIANGLE_FAN | 扇形三角形,共享中线点 |
1.3 纹理
纹理可以理解为存储在显存中图像,可以通过UV坐标映射到3D模型的表面,为图形渲染带来了丰富的细节和真实感。
上一篇介绍过纹理坐标,原点在左下,但图像存储时是以左上为(0,0),所以一般会遇到上下颠倒的问题,可以在shader中处理,也可以把纹理坐标倒过来放。
1.3.1 纹理的创建
// 纹理加载
GLuint texture;
glGenTextures(1, &texture);
// 绑定到当前活跃的纹理单元(Active Texture Unit中的指定纹理目标(如 `GL_TEXTURE_2D`)
glBindTexture(GL_TEXTURE_2D, texture);
// 设置环绕模式,超过边界时的表现
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// 设置过滤模式,放大缩小的表现
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
-
环绕模式
模式 行为 示意图 GL_REPEAT平铺重复 ▭▭▭▭ GL_CLAMP_TO_EDGE边缘拉伸 ▭→边缘延伸 GL_MIRRORED_REPEAT镜像重复 ▭8▭▭8▭ -
过滤模式
模式 描述 适用场景 GL_NEAREST最近邻采样,保留锐利边缘 像素风格 GL_LINEAR双线性插值,平滑过渡 常规3D模型 GL_LINEAR_MIPMAP_LINEAR三线性过滤 高质量渲染 -
多级渐远纹理(Mipmap) Mipmap是一系列不同分辨率的纹理图像,每个图像的尺寸是前一个的一半。例如用于不同距离下采样,可以减少锯齿和闪烁现象,同时还能优化性能。
使用也特别简单,在创建完一个纹理后调用glGenerateMipmap,OpenGL就会承担接下来的所有工作了。
glGenerateMipmap(GL_TEXTURE_2D);
纹理数据的填充,可以直接从CPU copy,也可以如iOS相机共享内存,还可以绑定FrameBuffer,作为画布填充中间内容。下文会讲图片到纹理和相机帧到纹理。
1.3.1 图片读取(通用)
Texture的数据填充,其实就是从CPU的spriteData Copy数据即可。
glTexImage2D(
GL_TEXTURE_2D, // 目标
0, // Mipmap级别(0为基级)
GL_RGBA, // 内部存储格式
width, height, // 纹理尺寸
0, // 历史遗留参数
format, // 数据格式
GL_UNSIGNED_BYTE, // 数据类型
data // 像素数据指针
);
但对于像素指针data,图片通常需要一下过程,这一步骤实际上是在给图片解码转换为位图。PNG格式为无损压缩,JPEG为有损压缩,过程和视频编码的帧内编码很类似。
解码这个过程也很耗时的,所以iOS的一些框架,如 YYImage和SDImage,在下载成功后也会进行如下类似步骤,目的就是在异步解码,从而在显示图片时更加流畅。
//1、将 UIImage 转换为 CGImageRef
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
//判断图片是否获取成功
if (!spriteImage) {
exit(1);
}
// 读取图片的大小,宽和高
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);
// 获取图片字节数 宽*高*4(RGBA)
GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
// 创建上下文
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
// 在CGContextRef上--> 将图片绘制出来
CGRect rect = CGRectMake(0, 0, width, height);
// 使用默认方式绘制
CGContextDrawImage(spriteContext, rect, spriteImage);
// 画图完毕就释放上下文
CGContextRelease(spriteContext);
1.3.2 iOS相机读取(共享内存)
在看GPUImage代码时,会发现纹理创建是通过以下方式,Core Video实现了CPU和GPU共享内存的方式可以实现高效零拷贝操作:
if ([GPUImageContext supportsFastTextureUpload]) {
// CVOpenGLESTextureCacheCreateTextureFromImage
} else {
// generateTexture
}
主要步骤包括:
- 创建Core Video纹理缓存
CVOpenGLESTextureCacheRef textureCache;
CVReturn err = CVOpenGLESTextureCacheCreate(
kCFAllocatorDefault,
NULL,
glContext,
NULL,
&textureCache
);
- 创建纹理(
yTexName),pixelBuffer在相机部分会有说明
CVOpenGLESTextureRef yTexture;
CVReturn yRet = CVOpenGLESTextureCacheCreateTextureFromImage(
kCFAllocatorDefault,
textureCache,
pixelBuffer,
NULL,
GL_TEXTURE_2D,
GL_LUMINANCE,
(GLsizei)CVPixelBufferGetWidth(pixelBuffer),
(GLsizei)CVPixelBufferGetHeight(pixelBuffer),
GL_LUMINANCE,
GL_UNSIGNED_BYTE,
0,
&yTexture
);
GLuint yTexName = CVOpenGLESTextureGetName(yTexture);
// 获取UV纹理