Hello OpenGL--003:离屏渲染

2020-08-12  本文已影响0人  e521

一、画面撕裂

1.1画面撕裂的形成

在介绍离屏渲染之前我们先了解一下什么是画面撕裂,以及其形成的原因:

画面撕裂

在游戏中我们有时会遇到这样的画面,我们很明显的能看到画面存在撕裂问题,其形成的原因是: GPU渲染之后会将结果放在 帧缓存区 中,视频控制器再通过读取帧缓存区中的数据进行 数模转换 来显示在屏幕上,显示的过程是从上至下逐行扫描进行显示,如下图画面撕裂的形成过程:假设只有一个帧缓存区的情况下,帧缓存区首先放了图1,屏幕首先对图1进行由上至下的扫描,但在扫描到图2的位置时,GPU又渲染了一张新的图片放到了帧缓存区中(图3),此时屏幕将会继续图2的位置进行扫描帧缓存区中的图片,即是此时的图3,则屏幕通过由上至下的扫描最终得到的结果就将是图4展示的样子,此时即形成了画面撕裂

画面撕裂的形成过程
1.2苹果解决画面撕裂的策略

苹果为应对画面撕裂问题采取了垂直同步+双缓存的策略。
垂直同步(Vertical synchronization):在扫面的过程中加入垂直同步信号,确保只有当前帧的图片扫面完成之后才会继续扫面下一帧的图片。
双缓存区:即采用两个缓存区来存储图片,屏幕交替扫描两个缓存区来进行显示。

苹果官方关于双缓存区示意图

虽然垂直同步+双缓存的策略解决了画面撕裂问题,但同时也引入了另一个问题:掉帧掉帧最直观的体现就是屏幕的卡顿,其形成的原因是:当接收到垂直同步信号的时候,CPUGPU还没有准备好相应的数据,即此时帧缓存区(FrameBuffer)不存在将要显示的数据,视频控制器拿不到新的数据,就会重复对上一帧的数据进行渲染

掉帧形成的示意图

为了应对掉帧问题,人们又采用的三缓存区,但掉帧归根结底的主要原因是CPUGPU处理速度问题,三缓存区虽然能在一定程度上抑制掉帧问题,但并不能从根本上解决。

1.3屏幕卡顿的原因

二、离屏渲染

2.1离屏渲染的触发

我们一般认为圆角会触发离屏渲染,但设置圆角就一定会触发离屏渲染吗?首先我们来看一个简单的demo:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //1.按钮存在背景图片
    UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn1.frame = CGRectMake(100, 130, 100, 100);
    btn1.layer.cornerRadius = 50;
    [self.view addSubview:btn1];
    [btn1 setImage:[UIImage imageNamed:@"image"] forState:UIControlStateNormal];
    btn1.clipsToBounds = YES;
    
    //2.按钮不存在背景图片
    UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
    btn2.frame = CGRectMake(100, 280, 100, 100);
    btn2.layer.cornerRadius = 50;
    btn2.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn2];
    btn2.clipsToBounds = YES;
    
    //3.UIImageView 设置了图片+背景
    UIImageView *imageV1 = [[UIImageView alloc] init];
    imageV1.frame = CGRectMake(100, 430, 100, 100);
    imageV1.backgroundColor = [UIColor blueColor];
    [self.view addSubview:imageV1];
    imageV1.layer.cornerRadius = 50;
    imageV1.layer.masksToBounds = YES;
    imageV1.image = [UIImage imageNamed:@"image"];
    
    //4.UIImageView 只设置了图片 无背景色
    UIImageView *imageV2 = [[UIImageView alloc] init];
    imageV2.frame = CGRectMake(100, 580, 100, 100);
    [self.view addSubview:imageV2];
    imageV2.layer.cornerRadius = 50;
    imageV2.layer.masksToBounds = YES;
    imageV2.image = [UIImage imageNamed:@"image"];
    
}

运行,并设置模拟器,Debug -> Color Off-screen rendered 标记出离屏渲染的部分:

离屏渲染提示 这样我们就会得到如下结果: 运行结果

被标记出黄色的部分是触发了离屏渲染的,而未被标记的则没有触发离屏渲染,由此可见设置了圆角不一定就会触发离屏渲染,那么触发离屏渲染的条件到底是什么呢?

2.2离屏渲染的探究
通常情况下的渲染流程是这样的: APP渲染流程

APP通过CPUGPU的合作,不断的将渲染的内容放到帧缓冲区(Frame Buffer)中,屏幕不断的从帧缓冲区中拿到要展示的内容,实时的显示在屏幕上。

离屏渲染的流程是这样的:

离屏渲染流程

与普通的渲染不同,离屏渲染需要创建额外的离屏渲染缓冲区(offscreen Buffer),将渲染好的内容放入其中,再等到合适的时机将离屏渲染缓冲区中的内容进行叠加、合并,之后再放入帧缓冲区中。
从流程图我们可以看出,离屏渲染时,需要APP提前将部分渲染能容保存到离屏渲染缓冲区,必要的时候需要对Offscreen BufferFrame Buffer进行切换,所以势必需要更多的处理时间,而且由于离屏渲染需要开辟额外的空间,大量的离屏渲染对势必也会消耗大量的内存。与此同时,离屏渲染缓冲区也是有大小限制的,不能超过屏幕像素点的2.5倍。
大量的离屏渲染容易造成掉帧,所以很多情况下我们能避则避。但有时我们需要实现一些特殊的效果,需要Offscreen Buffer保存渲染的中间状态时,我们也不得不使用离屏渲染
以苹果提供的毛玻璃效果UIBlurEffectView为例:

UIVisualEffectView with UIBlurEffect Rendering passes

整个过程需要经历,渲染内容->捕获内容->水平模糊->垂直模糊->合并形成毛玻璃效果,根据我们对帧缓冲区的了解,为节省空间,帧缓冲区中的内容绘制到屏幕上之后就会直接移除,无法做到如此复杂的特效,该过程需要在离屏缓冲区进行处理。
有时我们也会为了提高复用效率通过layer的光栅化 shouldRasterize主动开启离屏渲染,苹果关于shouldRasterize的解释如下:

When the value of this property is YES , the layer is rendered as a bitmap in its local coordinate space and then composited to the destination with any other content.

开启光栅化后,会触发离屏渲染Render Server 会强制将 CALayer的渲染位图结果bitmap保存下来,这样下次再需要渲染时就可以直接复用,从而提高效率。
而保存的 bitmap 包含layer 的 subLayer、圆角、阴影、组透明度 group opacity 等,所以如果layer的构成包含上述几种元素,结构复杂且需要反复利用,那么就可以考虑打开光栅化
圆角、阴影、组透明度等会由系统自动触发离屏渲染,那么打开光栅化可以节约第二次及以后的渲染时间。而多层 subLayer 的情况由于不会自动触发离屏渲染,所以相比之下会多花费第一次离屏渲染的时间,但是可以节约后续的重复渲染的开销。
shouldRasterize的使用也有一定的限制:

由上图我们可以看出layer由三部分组成,通常我们设置圆角会设置layercornerRadius,关于cornerRadiusapple的解释如下:

Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.

由上述可知,如果我们只是设置了cornerRadius属性,并不会对content进行裁剪,只有我们设置masksToBounds才会对内容进行裁剪。
图层的叠加大致遵循“画家算法”,即由远及近绘制图层显示在屏幕上:

画家算法

我们可以试想下:如果我们要对一个拥有多个图层构成的视图进行圆角设置,如果是存在帧缓冲区,那么就会存在一个问题,每渲染一帧就会丢前面的一帧数据,当我们要设置圆角时,前面的图层早已丢失,而离屏缓存区不同,离屏缓存区会对渲染的图层保留一段时间,这段时间就足以我们对多图层进行、合并、设置圆角等操作。想要触发离屏渲染不单单是说设置了masksToBounds就会触发,我们更多的要在意的是我们所要操作的图层,是否需要保留中间图层,如果只是单图层,肯定不会触发离屏渲染
值得注意的是,重写 drawRect: 方法并不会触发离屏渲染。重写 drawRect:会将 GPU中的渲染操作转移到 CPU中完成,并且需要额外开辟内存空间。

2.3圆角处理的参考方案
- (UIImage *)roundedCornerImageWithCornerRadius:(CGFloat)cornerRadius {
    CGFloat w = self.size.width;
    CGFloat h = self.size.height;
    CGFloat scale = [UIScreen mainScreen].scale;
    //防止圆角半径小于0,或者大于宽/高中较小值的一半。
    if (cornerRadius < 0) {
        cornerRadius = 0;
    }else if (cornerRadius > MIN(w, h)/2.0){
        cornerRadius = MIN(w, h)/2.0;
    }
    
    UIImage *image = nil;
    CGRect imageFrame = CGRectMake(0, 0, w, h);
    UIGraphicsBeginImageContextWithOptions(self.size, NO, scale);
    [[UIBezierPath bezierPathWithRoundedRect:imageFrame cornerRadius:cornerRadius] addClip];
    [self drawInRect:imageFrame];
    image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}
+ (UIImage *)addMaskToBounds:(CGRect)maskBounds image:(UIImage *)image cornerRadius:(CGFloat)cornerRadius {
    CGFloat w = maskBounds.size.width;
    CGFloat h = maskBounds.size.height;
    CGSize size = maskBounds.size;
    CGFloat scale = [UIScreen mainScreen].scale;
    CGRect imageRect = CGRectMake(0, 0, w, h);
    if (cornerRadius < 0) {
        cornerRadius = 0;
    }else if (cornerRadius > MIN(w, h)/2.0){
        cornerRadius = MIN(w, h)/2.0;
    }
    UIGraphicsBeginImageContextWithOptions(size, NO, scale);
    [[UIBezierPath bezierPathWithRoundedRect:imageRect cornerRadius:cornerRadius] addClip];
    [image drawInRect:imageRect];
    image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

@interface RoundImageView()
@property (nonatomic, strong) UIImageView *maskImageView;
@end

@implementation RoundImageView

- (instancetype)init {
    self = [super init];
    if (self) {
        _maskImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
        _maskImageView.image = [UIImage imageNamed:@"ic_imageView_mask"];//加圆角图片盖在上面
        [self addSubview:_maskImageView];
    }
    return self;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    CGRect bounds = self.bounds;
    _maskImageView.frame = bounds;
}

另附:YYImage的圆角处理


YYImage的圆角处理
2.4常见触发离屏渲染的几种情况
上一篇下一篇

猜你喜欢

热点阅读