分享

关于脏矩形的那些事

 勤奋不止 2012-02-17

关于脏矩形的那些事

(2011-12-30 20:06:15)
标签:

杂谈

分类: GameProgramming
很多人对脏矩形算法有所误解,其主要原因在于:
(1) 认为脏矩形算法是2D像素游戏时代的古老产物,而现在多数游戏都跑在3D引擎上。
(2) 脏矩形算法一般都有底层框架或引擎支持,上层应用/游戏开发者不需要考虑脏矩形问题。
对于以上两个问题,我的回答:
(1) 在使用3D渲染2D的情况下,脏矩形算法依然有效(比如:通过OpenGL/Direct3D纹理贴图来实现2D图像绘制,使用脏矩形技术可以大幅降低光栅化所需的时间。其实Android Surface就是这么做的,如:脏矩形链可以当成纹理扣出来等。)
(2) 虽然很多引擎本身支持脏矩形,但当你需要对引擎进行修改或扩展时,必须了解该引擎对脏矩形的支持程度,以及背后的运作机制。

声明:我这里只是提供大致思路(甚至自己都没有写过测试代码,主要是没时间...),如果所述有误,希望指正,谢谢。

在讲述脏矩形概念之前,先来看一下游戏图形渲染的基本流程:
while(1){
    [清屏 -> 使用画家算法把当前帧的所有物体画到屏幕的后台缓冲区上]
     ->
    [翻转后台缓冲区和屏幕缓冲区]
}
对于2D游戏或UI系统来说,渲染流程主要就分为以上2个阶段。(这里假设游戏是全屏独占的,对于非全屏独占情况类似,但底层实现有较大差别)
事实上,第一阶段和第二阶段均可以被脏矩形算法优化,并且脏矩形算法必须要求double buffer支持。

1. 使用脏矩形算法优化第二阶段:

翻转前后台缓冲区,有两种方法。一种采用把内存数据直接Blt到显存中,另一种则是采用页面翻转。
之所以不采用页面翻转的原因在于:不是所有适配器支持页面翻转,且页面翻转需要渲染代码直接对VGA硬件显存读写,而VGA硬件总线一般会比较慢。
如果我们把物体绘制到后台缓冲(内存),然后再通过脏矩形Blt到显存,则可以大幅降低对VGA内存的访问。

算法流程(假设背景不变):

设置前台缓冲区背景(因为背景不变)
DirtyList初始化为空
while(1)
{
    //逻辑代码:
    ....

    //渲染代码:
    后台缓冲区清屏,并绘制后台缓冲区背景
    绘制后台缓冲区物体
    Add每个物体的矩形到DirtyList,同时对DirtyList进行切割合并
    把后台缓冲区中对应的脏矩形部分Blt到前台缓冲
    清空DirtyList,并按按当前物体重建DirtyList
}

以上算法的缺点是,每次必须清空整个后台缓冲区然后重新绘制背景和所有物体。
但我们已假定背景不变,其实只要重画所有物体即可(不需要重新画背景。注:这里并不关心物体本身是否是脏的),于是得到进一步的优化:
设置前台缓冲区背景
设置后台缓冲区背景
DirtyList初始化为空
while(1)
{
    //逻辑代码:
    ....

    //渲染代码:
    Add每个物体的矩形到DirtyList,同时对DirtyList进行切割合并
    把这个DirtyList中的所有矩形作为后台缓存的剪裁区域
    后台缓冲区清屏,并绘制后台缓冲区背景(实际上只操作了剪裁区域)
    绘制后台缓冲区物体(实际上只操作了剪裁区域)
    把后台缓冲区中对应的脏矩形部分Blt到前台缓冲
    清空DirtyList,并按按当前物体重建DirtyList
}

以上算法,在Android里的实现方式如下:
public void run() {
    int frameCount = 0;
    Rect dirtyRect = new Rect(0,0,0,0);

    while(1){
        //逻辑代码:

        //渲染代码:
        if(frameCount++==0){
            canvas = Holder.lockCanvas(); //设置全屏刷新,相当于设置脏矩形区域为整个屏幕
        }else{
            把所有物体的矩形加入dirtyRect,得到并集 //注:Android API并不支持脏矩形链,但其实是可以用脏矩形链实现不规则的dirtyRects的
            canvas = Holder.lockCanvas(dirtyRect); //设置部分刷新
        }

        ...//清空并绘制背景
        ...//绘制所有物体
        dirtyRect.set(0,0,0,0); //清空矩形
        把所有物体的矩形加入dirtyRect,重建dirtyRect
        Holder.unlockCanvasAndPost(canvas);
    }
}

注1:一般来说,游戏的背景不会一成不变,但很少每一帧都在变,在这种我们只要在背景变化前重设脏矩形区域为整个屏幕区域即可。
注2:把所有物体的矩形加入dirtyRect,重建dirtyRect 这一步也可以放到下一帧的逻辑代码前面。(因为只有在逻辑代码里修改物体状态才会导致脏矩形改变)
注3:Android里Holder.lockCanvas(dirtyRect)的意思是说,后面的绘图操作使用dirtyRect作为clip区域,并且在最后swap前后台缓冲时只刷新dirtyRect区域,并且刷新完了之后下一帧lock到的Canvas上的buffer数据除了dirtyRect以外都是上一帧画完时的数据。(dirtyRect因为本来就要重画,所以这块区域Android不会保存)


2. 使用脏矩形算法优化第一阶段:
在(1.)中的算法必须要重画所有物体,并且每帧的脏区的总和涉及了至少所有的物体(节省了第二阶段的时间以及第一阶段中绘制背景的时间)。而事实上游戏中的物体大多都是不变的,比如:TileGame中的树木、房子等,
在这种情况下,我们希望这些物体只绘制到后台缓存一次,以后除非物体状态被改变(或者与该物体重叠的区域上的物体状态被改变),否则就不再把这些物体再次绘制到后台缓存上。
由于一个物体是否要绘制到后台缓存取决于逻辑代码(物体状态),比如:游戏的上层逻辑 (游戏开发者)调用引擎的API接口去控制物体的位置、大小、动画帧索引,那么游戏的下层逻辑(引擎)就需要在这些API的实现里标记这些物体是脏的(MarkDirty)。
MarkDirty()函数:
    i.   标记该物体的是脏的(mDirty=true),
    ii.  把该物体的矩形加入DirtyList
    iii. 检查是否有重叠的物体并标记这些重叠物体是脏的(但不需要care这些重叠区域,也不要扩大脏矩形区域)
    iv.  对DirtyList进行切割合并。
注意:物体状态的改变主要分两种:一种只是Sprite本身图片改变,如:按钮被按下(image),另一种是位置、大小或可见性等属性改变(resize),这两种最好分开处理。

public void run() {
    初始化1个背景单位到ObjectList,并call MarkDirty()  //背景可以作为一个物体处理
    初始化9个单位到ObjectList,并call MarkDirty()    //ObjectList是Z-Order排序的
    Rect dirtyRect = new Rect(0,0,0,0);

    while(1)
    {
        //逻辑代码(内部可能处理InputEvent):
        for(all Object in ObjectList){
            thisObject.Update();
                //Update函数内部:
                //  if(某个物体只是image需要改变){
                //      thisObject.MarkDirty();
                //  }else if(某个物体resize了){
                //      thisObject.MarkDirty();
                //      thisObject.scale = xxx;
                //      thisObject.position = xxx;
                //      thisObject.image = xxx;
                //      thisObject.MarkDirty();
                //  }else{
                //      物体状态没有改变,什么都不做
                //  }
               
        }


        //渲染代码:
        dirtyRect.set(0,0,0,0) 同时合并把DirtyList -> dirtyRect //注:Android API并不支持脏矩形链,但其实是可以用脏矩形链实现不规则的dirtyRects的
        canvas = Holder.lockCanvas(dirtyRect); //设置部分刷新
        for(all Object in ObjectList && the Object is dirty){
            绘制物体到后台缓冲区
        }
        Holder.unlockCanvasAndPost(canvas);
        清空DirtyList,所有物体的mDirty标记设为false
    }
}

注1:这种做法使得DirtyList同时依赖于渲染层和逻辑层,因此引擎需要对其进行封装,使其对开发者不可见。
注2:对thisObject.MarkDirty函数可作进一步优化(具体指"iii.检查是否有重叠的物体并标记这些物体是脏的"这一步):
    if(thisObject.mHasAlpha==true){
        ...//按照原来的做法,检查是否有重叠的物体并标记这些物体是脏的()
    }else{
        只有在ObjectList中thisObject之后的与thisObject矩形相交的object才需要设置dirty标记,在ObjectList中thisObject之前的那些object不需要设置dirty(后面的object说明后渲染,叠在上面)
    }


但这样的优化在resize时会出错!因为resize时,原物体位置可能变化,空出的部分必须被下面的物体重新画,改进的方法,是设置一个MarkDirtyFull的函数:
MarkDirtyFull和MarkDirty的区别在于:
MarkDirty会判断object.mHasAlpha是否等于true,如果等于true就调用MarkDirtyFull(检查是否有重叠的物体并标记这些物体是脏的不管是否有alpha)
如果object.mHasAlpha等于false,按照上面的做法,这样可以避免在resize时出错。

public void run() {
    初始化1个背景单位到ObjectList,并call MarkDirty()  //背景可以作为一个单位处理
    初始化9个单位到ObjectList,并call MarkDirty()    //ObjectList是Z-Order排序的
    Rect dirtyRect = new Rect(0,0,0,0);

    while(1)
    {
        //逻辑代码(内部可能处理InputEvent):
        for(all Object in ObjectList){
            thisObject.Update();
                //Update函数内部:
                //  if(某个物体只是image需要改变){
                //      thisObject.MarkDirty();
                //  }else if(某个物体resize了){
                //      thisObject.MarkDirtyFull(); //Resize时必须MarkDirtyFull
                //      thisObject.scale = xxx;
                //      thisObject.position = xxx;
                //      thisObject.image = xxx;
                //      thisObject.MarkDirty();
                //  }else{
                //      物体状态没有改变,什么都不做
                //  }
        }


        //渲染代码:
        dirtyRect.set(0,0,0,0) 同时合并把DirtyList -> dirtyRect //注:Android API并不支持脏矩形链,但其实是可以用脏矩形链实现不规则的dirtyRects的
        canvas = Holder.lockCanvas(dirtyRect); //设置部分刷新
        for(all Object in ObjectList && the Object is dirty){
            绘制物体到后台缓冲区
        }
        Holder.unlockCanvasAndPost(canvas);
        清空DirtyList,所有物体的mDirty标记设为false
    }
}

注3:对thisObject.MarkDirtyFull函数可作进一步优化:
    遍历在ObjectList中thisObject之前的object,
        如果之前的object中有一个mHasAlpha=false,并且完全包含thisObject,那么只需要标记dirty到这个object为止,不再标记更底层的object
        否则就标记ObjectList中thisObject之前的所有object
    遍历在ObjectList中thisObject之后的object,
        全部标记dirty,只要有交集


3. Android中的脏矩形
Android Surface自带脏矩形刷新的API,以SurfaceView为例就是:
SurfaceHolder.java
      Start editing the pixels in the surface.  The returned Canvas can be used
      to draw into the surface's bitmap.  A null is returned if the surface has
      not been created or otherwise can not be edited.  You will usually need
      to implement {@link Callback#surfaceCreated Callback.surfaceCreated}
      to find out when the Surface is available for use.
    
      <p>The content of the Surface is never preserved between unlockCanvas() and
      lockCanvas(), for this reason, every pixel within the Surface area
      must be written. The only exception to this rule is when a dirty
      rectangle is specified, in which case, non dirty pixels will be
      preserved.
    
      <p>If you call this repeatedly when the Surface is not ready (before
      {@link Callback#surfaceCreated Callback.surfaceCreated} or after
      {@link Callback#surfaceDestroyed Callback.surfaceDestroyed}), your calls
      will be throttled to a slow rate in order to avoid consuming CPU.
    
      <p>If null is not returned, this function internally holds a lock until
      the corresponding {@link #unlockCanvasAndPost} call, preventing
      {@link SurfaceView} from creating, destroying, or modifying the surface
      while it is being drawn.  This can be more convenience than accessing
      the Surface directly, as you do not need to do special synchronization
      with a drawing thread in {@link Callback#surfaceDestroyed
      Callback.surfaceDestroyed}.
    
      @return Canvas Use to draw into the surface.


    public Canvas lockCanvas();


   
      Just like {@link #lockCanvas()} but allows to specify a dirty rectangle.
      Every
      pixel within that rectangle must be written; however pixels outside
      the dirty rectangle will be preserved by the next call to lockCanvas().
    
      @see android.view.SurfaceHolder#lockCanvas
    
      @param dirty Area of the Surface that will be modified.
      @return Canvas Use to draw into the surface.


    public Canvas lockCanvas(Rect dirty);


由此可见,Android中的脏矩形接口只能优化渲染流程的第二步,并且不支持脏矩形链(至少Java接口是不支持脏矩形链的。但Native层可以,比如:使用OpenGL模板缓存区或打开裁剪测试来实现)。如果需要优化渲染流程的第一步,则需要在Android Surface View之上设计一个引擎。

SurfaceHolder.lockCanvas(); //得到一块画布作为back buffer,该back buffer不一定是前一帧的back buffer
SurfaceHolder.lockCanvas(new Rect(0,0,0,0)); //得到一块画布作为back buffer,就是前一帧back buffer
SurfaceHolder.lockCanvas(new Rect(100,100,100,100)); //得到一块画布作为back buffer,该back buffer中Rect(100,100,100,100)区域外的内容是前一帧back buffer

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多