03*:离屏渲染、OpenGL渲染结构、固定着色器种类、图元
问题
目录
1:离屏渲染
2:OpenGL渲染结构
3:定着色器种类
4:图元
预备
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. //1.按钮存在背景图片 UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom]; btn1.frame = CGRectMake(100, 30, 100, 100); btn1.layer.cornerRadius = 50; [self.view addSubview:btn1]; [btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal]; btn1.clipsToBounds = YES; //2.按钮不存在背景图片 UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom]; btn2.frame = CGRectMake(100, 180, 100, 100); btn2.layer.cornerRadius = 50; btn2.backgroundColor = [UIColor blueColor]; [self.view addSubview:btn2]; btn2.clipsToBounds = YES; //3.UIImageView 设置了图片+背景色; UIImageView *img1 = [[UIImageView alloc]init]; img1.frame = CGRectMake(100, 320, 100, 100); img1.backgroundColor = [UIColor blueColor]; [self.view addSubview:img1]; img1.layer.cornerRadius = 50; img1.layer.masksToBounds = YES; img1.image = [UIImage imageNamed:@"btn.png"]; //4.UIImageView 只设置了图片,无背景色; UIImageView *img2 = [[UIImageView alloc]init]; img2.frame = CGRectMake(100, 480, 100, 100); [self.view addSubview:img2]; img2.layer.cornerRadius = 50; img2.layer.masksToBounds = YES; img2.image = [UIImage imageNamed:@"btn.png"]; }
正文
一:离屏渲染
1:模拟器打开离屏渲染颜色标注
imageView变成了黄色,说明出现了 离屏渲染。
1:背景颜色、layer、mask 离屏渲染
imageView变成了黄色,说明出现了 离屏渲染。
imageView.backgroundColor = [UIColor whiteColor]; imageView.layer.cornerRadius = 50; imageView.layer.masksToBounds = YES;
2:我们修改一下代码,去掉裁剪masksToBounds
imageView.backgroundColor = [UIColor whiteColor]; imageView.layer.cornerRadius = 50; // imageView.layer.masksToBounds = YES;
结果如下:content(image)没有裁剪,也没有看到离屏渲染
3:我们再修改一下代码,去掉backgroundColor
// imageView.backgroundColor = [UIColor whiteColor]; imageView.layer.cornerRadius = 50; imageView.layer.masksToBounds = YES;
黄色部分消失,所以说设置了 cornerRadius 未必会导致离屏渲染,
2:离屏渲染与正常渲染
屏幕上最终显示的数据有两种加载流程
- 正常渲染加载流程
-
离屏渲染加载流程
从图上看,他们之间的区别就是离屏渲染比正常渲染多了一个离屏缓冲区,这个缓冲区的作用是什么呢?下面来仔细说说
首先,说说正常渲染流程
3:正常渲染流程
APP中的数据经过CPU计算和GPU渲染后,将结果存放在帧缓冲区,利用视频控制器从帧缓冲区中取出,并显示到屏幕上。
- 在GPU的渲染流程中,显示到屏幕上的图像是遵循大画家算法按照由远及近的顺序,依次将结果存储到帧缓冲区
-
视屏控制器从帧缓冲区中读取一帧数据,将其显示到屏幕上后,会立即丢弃这帧数据,不会做任何保留,这样做的目的是可以节省空间,且在屏幕上是各自显示各自的,互相不影响。
4:离屏渲染流程
当App需要进行额外的渲染和合并时,例如按钮设置圆角,我们是需要对UIButton这个控件中的所有图层都进行圆角+裁剪,然后再将合并后的结果存入帧缓存区,再从帧缓存中取出交由屏幕显示,这时,在正常的渲染流程中,我们是无法做到对所有图层进行圆角裁剪的,因为它是用一个丢一个。所以我们需要提前将处理好的结果放入离屏缓冲区,最后将几个图层进行叠加合并,存放到站缓冲区,最后屏幕上就是我们想实现的效果。
说白了,离屏缓存区就是一个临时的缓冲区,用来存放在后续操作使用,但目前并不使用的数据。
- 离屏渲染再给我们带来方便的同时,也带来了严重的性能问题。由于离屏渲染中的离屏缓冲区,是额外开辟的一个存储空间,当它将数据转存到Frame Buffer时,也是需要耗费时间的,所以在转存的过程中,仍有掉帧的可能。
- 离屏缓冲区的空间并不是无限大的, 它是又上限的,最大只能是屏幕的2.5倍
那为什么我们明知有性能问题时,还是要使用离屏渲染呢?
- 可以处理一些特殊的效果,这种效果并不能一次就完成,需要使用离屏缓冲区来保存中间状态,不得不使用离屏渲染,这种情况下的离屏渲染是系统自动触发的,例如经常使用的圆角、阴影、高斯模糊、光栅化等
- 可以提升渲染的效率,如果一个效果是多次实现的,可以提前渲染,保存到离屏缓冲区,以达到复用的目的。这种情况是需要开发者手动触发的。
当我们开启光栅化时,会将layer渲染成位图保存在缓存中,这样在下次使用时,就可以直接复用,提高效率。
针对光栅化的使用,有以下几个建议:
- 如果layer不能被复用,则没有必要开启光栅化
- 如果layer不是静态,需要被频繁修改(例如动画过程中),此时开启光栅化反而影响效率
- 离屏渲染缓存内容有时间限制,如果100ms内没有被使用,那么就会丢弃,无法进行复用
- 离屏渲染的缓存空间有限,是屏幕的2.5倍,超过2.5倍屏幕像素大小的话也会失效,无法实现复用
5:圆角中离屏渲染的触发时机
backgroundColor、contents、borderWidth&borderColor
构成的。跟我们即将解释的圆角触发离屏渲染息息相关。总结
- 当只设置backgroundColor、border,而contents中没有子视图时,无论
maskToBounds / clipsToBounds
是true
还是false
,都不会触发离屏渲染 - 当contents中有子视图时,此时设置
cornerRadius
+maskToBounds / clipsToBounds
,就会触发离屏渲染, - 但是这种情况在UIImageView中并不适用,当UIImageView中只设置图片+
maskToBounds / clipsToBounds
是不会触发离屏渲染,苹果对UIImageView优化我想也只是将image直接画在了contents上面这样不设置背景色其实只需要渲染一个layer,所以不需要用到离屏缓冲区,
所以不会产生离屏渲染,如果此时再加上背景色,就会触发离屏渲染。
所以,综合来说,离屏渲染是否触发,在于我们是否需要使用离屏缓冲区。
6、离屏渲染出发的触发条件
我们只设置contents
或者UIImageView
的image
,并加上圆角+裁剪,是不会产生离屏渲染的。但如果加上了背景色、边框或其他有图像内容的图层,还是会产生离屏渲染。
- 使用了 mask 的 layer (layer.mask)
- 需要进行裁剪的 layer (layer.masksToBounds /view.clipsToBounds)
- 设置了组透明度为 YES,并且透明度不为 1 的layer (layer.allowsGroupOpacity/ layer.opacity)
- 添加了投影的 layer (layer.shadow*)
- 采用了光栅化的 layer (layer.shouldRasterize)
- 绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)
7.避免圆角离屏渲染
为了避免设置圆角时引起的离屏渲染操作,可以用一下方案代替直接设置圆角的操作
- 直接更换支援,让UI提供带圆角的图片。
- 使用layer.mask属性,增加一个和背景色相同的遮罩覆盖上层,盖住四个角,营造出圆角的形状。但这种方式难以解决背景色为图片或渐变色的情况。
- 使用贝塞尔曲线绘制闭合圆角的矩形,在上下文中设置只有内部可见,再将不带圆角的 layer 渲染成图片,添加到贝塞尔矩形中。这种方法效率更高,但是 layer 的布局一旦改变,贝塞尔曲线都需要手动地重新绘制,所以需要对 frame、color 等进行手动地监听并重绘。
- CoreGraphics重写 drawRect:,用 CoreGraphics 相关方法,在需要应用圆角时进行手动绘制。不过 CoreGraphics 效率也很有限,如果需要多次调用也会有效率问题。
1:为UIImage类扩展一个实例函数,仿YYImage做法,贝塞尔曲线
- (UIImage *)imageWithCornerRadius:(CGFloat)radius ofSize:(CGSize)size{ /* 当前UIImage的可见绘制区域 */ CGRect rect = (CGRect){0.f,0.f,size}; /* 创建基于位图的上下文 */ UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale); /* 在当前位图上下文添加圆角绘制路径 */ CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath); /* 当前绘制路径和原绘制路径相交得到最终裁剪绘制路径 */ CGContextClip(UIGraphicsGetCurrentContext()); /* 绘制 */ [self drawInRect:rect]; /* 取得裁剪后的image */ UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); /* 关闭当前位图上下文 */ UIGraphicsEndImageContext(); return image; }
使用时,对图片进行圆角处理
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(10, 10, 100, 100)]; /* 创建并初始化UIImage */ UIImage *image = [UIImage imageNamed:@"icon"]; /* 添加圆角矩形 */ image = [image imageWithCornerRadius:50 ofSize:imageView.frame.size]; [imageView setImage:image];
2:Core Graphics方式 用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100,100,100,100)]; imageView.image = [UIImage imageNamed:@"xx"]; //开始对imageView进行画图 UIGraphicsBeginImageContextWithOptions(imageView.bounds.size,NO,1.0); //使用贝塞尔曲线画出一个圆形图 [[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip]; [imageView drawRect:imageView.bounds]; imageView.image = UIGraphicsGetImageFromCurrentImageContext(); //结束画图 UIGraphicsEndImageContext(); [self.view addSubview:imageView];
3:CAShapeLayer 方式
使用CAShapeLayer(属于CoreAnimation)与贝塞尔曲线可以实现不在view的drawRect方法中画出一些想要的图形,CAShapeLayer动画渲染直接提交GPU当中,相较于view的drawRect方法使用CPU渲染而言,其效率高,能大大优化内存使用
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; imageView.image = [UIImage imageNamed:@"myImg"]; UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size]; CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init]; //设置大小 maskLayer.frame = imageView.bounds; //设置图形样子 maskLayer.path = maskPath.CGPath; imageView.layer.mask = maskLayer; [self.view addSubview:imageView];
二:OpenGL渲染结构
OpenGL中的渲染架构如图所示
主要分为两个模块
- Client:是指常见的iOS代码和OpenGL API方法,这部分是在CPU中运行
- Server:是指OpenGL底层的渲染等处理,是运行在GPU中的
架构分析
- 客户端中通过iOS代码调用OpenGL API中的方法,将图形渲染的相关数据通过通道传递到服务器中顶点着色器和片元着色器,并交由GPU处理。
- 服务器通过与客户端的通道接收传递的数据,并交由相应着色器进行渲染处理,并将最终的结果渲染到屏幕上
数据传递
从图上我们可以看出,客户端和服务器进行数据传递的通道有三种
- Attributes
- Uniform
- Texture Data
Attributes
- Attributes通道只能将数据直接传递到顶点着色器,不能直接传递到片元着色器,但是可以通过顶点着色器间接传递给片元着色器。
- 通过Attributes传递的通常是经常发生变化的数据,例如颜色、顶点等。
- Attribute主要传递这些参数:颜色数据、顶点坐标、纹理坐标、光照法线等。
Uniform
- Uniform通过既可以传递到顶点着色器,也可以传递到片元着色器。
- Uniform中传递的通常是比较统一的批次数据,不经常发生变动的数据。
Texture Data
- Texture Data同Unoform一样,可以将数据传递到顶点和片元着色器。
- 由于顶点着色器主要是处理顶点数据的,我们将纹理数据传过去并没有多大的意义。而纹理的处理的逻辑主要是在片元着色器中进行的。
渲染流程总结
- 设置顶点数据和其他参数。
- 在顶点着色器中进行运算得到裁剪坐标。
- 细分着色器、几何着色器,不可自定义,跳过。
- 图元设置,根据设置构成点、线、三角形。
- 裁剪,裁剪掉超出显示区域的部分。
- 光栅化, 将图源栅格化为一个个的像素点。
- 片元着色器,将对应的栅格(像素)填充为具体的颜色。
- 渲染图像。
三:固定管线下的着色器
OpenGL 固定管线下为开发者提供了几种着色器:单元着色器、平面着色器、上色着色器、默认光源着色器、点光源着色器、纹理替换矩阵着色器、纹理调整着色器、纹理光源着色器。下面依次介绍API和参数说明。
2.1 单元着色器
/**
使用场景:绘制默认OpenGL 坐标系(-1,1)下图形. 图形所有片段都会以一种颜色填充
参数1: 存储着⾊色器器种类-单元着⾊器
参数2: 颜⾊
**/
GLShaderManager::UserStockShader(GLT_SHADER_IDENTITY,GLfloat vColor[4]);
2.2 平面着色器
/**
使用场景:在绘制图形时, 可以应⽤变换(模型/投影变化)
参数1: 存储着⾊器种类-平面着⾊器
参数2: 允许变化的4*4矩阵
参数3: 颜⾊
**/
GLShaderManager::UserStockShader(GLT_SHADER_FLAT,GLfloat mvp[16],GLfloat vColor[4]);
2.3 上色着色器
/**
使用场景:在绘制图形时, 可以应用变换(模型/投影变化) 颜色将会平滑地插⼊入到顶点之间 称为平滑着色.
参数1: 存储着色器种类-上色着色器
参数2: 允许变化的4*4矩阵
**/
GLShaderManager::UserStockShader(GLT_SHADER_SHADED,GLfloat mvp[16]);
2.4 默认光源着色器
/**
使用场景:在绘制图形时, 可以应⽤用变换(模型/投影变化) 这种着⾊器会使绘制的图形产生 阴影和光照的效果
参数1: 存储着色器种类-默认光源着色器
参数2: 模型4*4矩阵
参数3: 投影4*4矩阵
参数4: 颜色值
**/
GLShaderManager::UserStockShader(GLT_SHADER_DEFAULT_LIGHT,GLfloat mvMatrix[16],GLfloat pMatrix[16],GLfloat vColor[4]);
2.5 点光源着色器
/**
使用场景:在绘制图形时, 可以应⽤用变换(模型/投影变化) 这种着色器会使绘制的图形产生阴影和光照的效果.它与默认光源着⾊器⾮常类似,区别只是光源位置可能是特定的.
参数1: 存储着⾊器种类-点光源着色器 参数2: 模型4*4矩阵
参数3: 投影4*4矩阵
参数4: 点光源的位置
参数5: 漫反射颜色值
**/
GLShaderManager::UserStockShader(GLT_SHADER_POINT_LIGHT_DIEF,GLfloat mvMatrix[16],GLfloat pMatrix[16],GLfloat vLightPos[3],GLfloat vColor[4]);
2.6 纹理替换矩阵着色器
/**
使用场景:在绘制图形时, 可以应用变换(模型/投影变化)这种着⾊器通过给定的模型视图投影矩阵.使用纹理单元来进行颜⾊填充.其中每个像素点的颜色是从纹理中获取
参数1: 存储着⾊器种类-纹理替换矩阵着⾊器
参数2: 模型4*4矩阵
参数3: 纹理单元
**/
GLShaderManager::UserStockShader(GLT_SHADER_TEXTURE_REPLACE,GLfloat mvMatrix[16],GLint nTextureUnit);
2.7 纹理替换矩阵着色器
/**
使用场景:在绘制图形时, 可以应用变换(模型/投影变化)这种着⾊器通过给定的模型视图投影矩阵. 着⾊器将⼀个基本色乘以一个取自纹理单元nTextureUnit 的纹理.将颜色与纹理进行颜色混合后才填充到片段中.
参数1: 存储着⾊器种类-纹理调整着色器
参数2: 模型4*4矩阵
参数3: 颜色值
参数4: 纹理单元
**/
GLShaderManager::UserStockShader(GLT_SHADER_TEXTURE_MODULATE,GLfloat mvMatrix[16],GLfloat vColor[4],GLint nTextureUnit);
2.8 纹理光源着色器
/** 使用场景:在绘制图形时, 可以应用变换(模型/投影变化)这种着⾊器通过给定的模型视图投影矩阵. 着⾊器将一个纹理通过漫反射照明计算进行调整(相乘). 参数1: 存储着⾊器种类-纹理光源着⾊器 参数2: 模型4*4矩阵 参数3: 投影4*4矩阵 参数4: 点光源位置 参数5: 颜色值(⼏何图形的基本色) 参数6: 纹理单元 **/ GLShaderManager::UserStockShader(GLT_SHADER_TEXTURE_POINT_LIGHT_DIEF,G Lfloat mvMatrix[16],GLfloat pMatrix[16],GLfloat vLightPos[3],GLfloat vBaseColor[4],GLint nTextureUnit
四:7种基本图元
绘制类型是点:GL_POINTS
void SetupRC(void) { //1、 清空颜色 glClearColor(0.0f, 0.0f, 0.0f, 1.0f); //2、 初始化着色器管理类 shaderManager.InitializeStockShaders(); GLfloat vVertex_1[4][3] = { { -0.2f, -0.2f, 0.0f }, { -0.2f, 0.2f, 0.0f }, { 0.2f, 0.2f, 0.0f }, { 0.2f, -0.2f, 0.0f } }; //3、 设置要渲染的点 //GL_POINTS,点 triangleBatch.Begin(GL_POINTS, 4); //参数二:4个顶点 triangleBatch.CopyVertexData3f(vVertex_1); triangleBatch.End(); }
注意