渲染杂谈:early-z、z-culling、hi-z、z-perpass到底是什么?
之前一直被这几个和深度缓存(z-buffer)相关的概念搞得神魂颠倒。今天在翻阅《Real-Time Rendering》时碰巧碰巧看到了这部分的讲解。硬着头皮看了看,姑且算是讲几个概念分清楚了。以我的记性估计下周就全忘了,所以打算顺手记下来。
这四种技术本质上都是解决传统渲染管线中的同一个问题——过度绘制(OverDraw) 。一个经典的渲染管线通常会依次经历顶点阶段、光栅化、片元阶段和逐像素处理。其中片元阶段会进行复杂的光照计算,是整个管线的性能瓶颈。而在逐像素阶段会对计算出来的片元值进行各种测试以判断这个片元会不会最终显示到屏幕上。这就带来了一个矛盾:明明我在片元阶段花费了最大的力气计算出结果,但马上的逐像素阶段就可能将这个结果直接舍弃。而事实上逐像素阶段的深度测试(z-test)会舍弃大量片元,对于较为复杂的场景甚至会丢弃80%之多。如果我们能在片元阶段之前进行深度测试提前丢弃掉那些不需要绘制到屏幕上的片元,那么就可以减少大量片元计算提升效率。early-z、z-cull、hi-z、z-perpass都是为了解决这个问题而产生的不同技术。
1.early-z
early-z的解决方式非常简单,就是直接修改传统渲染管线,在光栅化和片元阶段中间,加入一个early-z阶段。这个阶段进行的操作和原本逐像素处理阶段的z-test(为了与early-z区别,这个阶段也会被成为late-z)操作完全一样,现代的gpu已经都开始包含这样的硬件设计。但是early-z有以下两个主要的缺点:
* 一旦进行了手动写入深度值、开启alpha test或者丢弃像素等操作,那么gpu就会关闭early-z直到下次clear z-buffer后才会重新开启(不过现在的gpu也在逐渐优化,使其更智能开关early-z)。之所以gpu会选择关闭early-z是因为上述那些操作可能会在片元阶段与late-z阶段之间修改深度缓存中的深度值,导致提前的early-z的结果并不正确。我们也可以在fragment shader中使用layout(early_fragment_tests)来强制打开early-z。
* early-z的优化效果并不稳定,最理想条件下所有绘制顺序都是由近及远,那么early-z可以完全避免过度绘制。但是相反的状态下,则会起不到任何效果。所以有些时候为了完全发挥early-z的功效,我们会在每帧绘制时对场景的物体按照到摄像机的距离由远及近进行排序。这个操作会在cpu端进行,当场景复杂到一定程度,频繁的排序将会占用cpu的大量计算资源。
2.z-culling
z-culling和early-z一样都是gpu硬件层面的优化,所以之前我一直混淆两者是同一种东西。两者最明显的区别是early-z是以pixel quad为单位(既以4个像素为一组,因为深度缓存内的数据是按Z字形排列的)逐个像素进行比较,而z-culling是以tile(比如16*16像素)为单位进行整体比较。这里又涉及到tile的概念,虽然我看到的资料中并没有提到,但是我认为这里的tile和tile based rendering(TBR)中的tile是同一概念。也就是说这种技术应该只应用于使用TBR架构的移动端gpu中。其主要方式取得当前tile所对应的的深度缓冲区中的Zmax和Zmin,如果该tile当前深度的最小值<Zmax,则说明整个tile都不可见将整个tile全部丢弃。如果该tile当前深度的最大值>Znim,则说明整个tile都处于最前面,保留整个tile,并因此可以省去该tile对应片元在late-z阶段对深度缓冲区的读取操作,直接写入就可以。对于其它情况,则交给后续的深度处理进行更细致的判断。由于z-culling通常用于TBR架构gpu,所以它也和TPR架构一样保持了对gpu带宽的敏感性。因此不同于early-z,z-culling并不会对深度缓存进行写入,也不会对深度缓存进行直接读取。它所需要的比对数据(Zmax和Zmin)都会储存在on-chip缓存中的某个固定区域,特点即是容量小但速度快。由于z-culling对深度缓存是只读的,因此不会因为手动写入深度值、开启alpha test或者丢弃像素等操作对其有影响,这刚好解决了early-z的第一个缺点。总结来说,z-culling利用TBR架构进行了非常粗粒度的提前深度测试,但不会带来额外的对于深度缓存进行读写消耗,因此也比z-early具有刚多的适用范围。
这里有一个疑问,为什么early-z不像z-culling一样,对深度缓存只读,来避免收到手动写入深度值、开启alpha test或者丢弃像素等操作的影响?其中一个解释是,在z-culling阶段后,那些没有被优化的片元在late-z阶段会读取深度缓存进行细粒度的测试,完成后再更新写入新的深度缓存。同时也会更新z-culling会访问的on-chip缓存。由于z-culling访问的是on-chip的所以不会带来额外开销,所以整体上只有对深度缓存进行一次读一次写。而对于early-z来说,如果在early-z阶段只读取深度缓存而不写入的话,那么在late-z阶段就需要重新读取然后写入,以更新深度缓存。这就相当于两次读一次写,带来了额外的开销。不过也看到有人说late-z阶段对深度缓存的读写是无论如何都会进行的,所以此处存疑。
还需要说明的是,z-culling和early-z都可以不依赖于对方单独存在,当然两者也可以共存。当两者共存的时候,会先进行z-culling做粗粒度的筛选,再进行early-z做细粒度的排除。在有些资料中也会把z-culling成为HiZ(没错,就是最后要讲的hi-z),这要是不弄混就怪了。
3.z-perpass
和上面两种技术不同,z-perpass是一种软件技术。它主要是配合early-z使用,来减少开始提到的early的第二个缺点——效果不稳定。其做法是将场景做两个pass的绘制。第一个pass仅写入深度,不做任何复杂的片元计算,不输出任何颜色。第二个pass关闭深度写入,并将深度比较函数设为“相等”。我在开篇有提到过度绘制的主要矛盾——经过大量运算的片元,很大概率会在之后被丢弃掉。那么对于第一个pass由于只写入深度,不在片元做任何计算,所以即便之后会被丢弃,也并不可惜。也就是说无论场景中的物体以怎样的顺序绘制,我们都可以以很小的代价提前绘制好当前场景的深度缓存。那么在第二个pass时,early-z就可以用这个深度缓存中的值和当前深度值进行比较,只绘制深度相等的片元,任何其他的片元都可以直接丢弃,因此第二个pass要把深度比较函数设为“相等”。同时当前的深度缓存已经是完全正确的结果了,因此第二个pass也不需要对深度缓存做任何更新,便可以关闭深度写入。
z-perpass必须配合early-z才能发挥效果,如果没有early-z的话,第二个pass的深度测试依旧在片元后,因此所有片元都会在片元阶段进行复杂计算。z-perpass的思想和延迟渲染管线(defered render pipeline,下面也会提到)有些相似,差别在于:第一,z-perpass的第一个pass只计算深度,并且结果直接存储在深度缓存。而延迟渲染会同时计算更多其他的屏幕空间数据,并将这些数据存储在额外的framebuffer中,需要更大的缓存(也就是GBuffer)。第二,z-perpass的第二个pass依旧需要对全场景的各个物体进行绘制(至少顶点阶段是如此),而延迟渲染的第二个pass类似于后处理本质上只绘制了一个屏幕大小的矩形。
4.hi-z
hi-z全名Hierarchical Z,和z-perpass一样也是一种软件技术,据说这项技术最早是在《刺客信条:大革命》中使用的。其核心原理是利用上一帧的深度图和摄像机矩阵,来对当前帧的场景做剔除,对于剔除后的物体进行绘制新的深度图和GBuffer,然后再用新的深度图和当前摄像机矩阵再对当前帧的场景做剔除,对剔除后的物体进行绘制更新刚刚的深度图和GBuffer。之所这种看起来十分复杂的方法能提高效率,是因为每一帧的绘制都已上一帧的绘制结果为基础。我们假设相邻两针差距不会特别大,那么以上一帧的深度图作为结果来对当前帧可见的物体进行筛选,可以得到绝大部分。而对于少量两针不一样的物体,进行第二次深度绘制,由于第二次绘制的少量不一样的物体所带来的的计算量很小,因此可以带来性能上的提升。这种基于前一帧的迭代式的对场景物体进行剔除,便可以在一定程度上减少过度绘制。不过由于我也没有实现过这种算法,所以对这种算法实际带来的效果存疑。
值得注意的是,这里提到了Gbuffer,那么说明hi-z技术是基于延迟渲染管线。而延迟渲染管线本身也是在减少各种由其他原因(包括但不限于深度测试这个原因)导致的过渡绘制。其目的就是希望无论拥有多少模型多少光源,整个场景渲染的复杂度都O(1)。延迟渲染管线本身也是个庞大的话题,可能以后会结合Unity刚刚正式更新的Scriptable Render Pipeline也写点东西。
终于把这四种技术讲完了,除了这四种在名字容易让人混淆的技术外,其实还有一些东西没有提到。对于TBR(或者TBDR)架构的gpu,因为其提供了提前优化的潜能所以各家硬件厂商也会有自己独特的针对于过度绘制的优化,比如PowerVR的HSR(Hidden Surface Removal)和Arm的APK(Forward Pixel Kill)。这些技术可以配合early-z等技术来更高效的避免过度绘制。
渲染杂谈:early-z、z-culling、hi-z、z-perpass到底是什么?的更多相关文章
- 给定一个字符串,把字符串内的字母转换成该字母的下一个字母,a换成b,z换成a,Z换成A,如aBf转换成bCg, 字符串内的其他字符不改变,给定函数,编写函数 void Stringchang(const char*input,char*output)其中input是输入字符串,output是输出字符串
import java.util.Scanner; /*** * 1. 给定一个字符串,把字符串内的字母转换成该字母的下一个字母,a换成b,z换成a,Z换成A,如aBf转换成bCg, 字符串内的其他字 ...
- Linux Ctrl+Z VS Ctrl+C 以及+Z的使用方法
问题及处理: Ctrl+Z是将任务中断,但是此任务并没有结束,他仍然在进程中他只是维持挂起的状态,用户可以使用fg/bg操作继续前台或后台的任务,fg命令重新启动前台被中断的任务,bg命令把被中断的任 ...
- 由link和@import的区别引发的CSS渲染杂谈
我们都知道,外部引入 CSS 有2种方式,link标签和@import. 它们有何本质区别,有何使用建议,在考察外部引入 CSS 这部分内容时,经常被提起. 如今,很多学者本着知其然不欲知其所以然的学 ...
- 深入剖析GPU Early Z优化
最近在公司群里同事发了一个UE4关于Mask材质的优化,比如在场景中有大面积的草和树的时候,可以在很大程度上提高效率.这其中的原理就是利用了GPU的特性Early Z,但是它的做法跟我最开始的理解有些 ...
- WPF 容器的Z顺序操作
当需要动态添加.修改.删除控件时,如果要达到最好的效果,肯定不只是把需要的控件添加到容器中,并且还需要把容器中的已有控件进行排序操作(置顶.置底.前移.后移操作).由于初次接触到wpf,所以对很多知识 ...
- 数字信号处理--Z变换,傅里叶变换,拉普拉斯变换
傅立叶变换.拉普拉斯变换.Z变换最全攻略 作者:时间:2015-07-19来源:网络 傅立叶变换.拉普拉斯变换.Z变换的联系?他们的本质和区别是什么?为什么要进行这些变换.研究的都是什么? ...
- 用宏 x y z,找出最大值最小值
#define max(x,y,z) ((x)>(y)?(x):(y))>(z)?((x)>(y)?(x):(y)):(z) #define min(x,y,z) ((x)<( ...
- JavaScript一道面试题求y的值是? z 的值是? s的值是?
原文:http://www.zhufengpeixun.cn/JavaScriptmianshiti/2014-04-01/287.html < script type = "text ...
- C#调用C++函数入口点的问题 z
C++使用 void extern __declspec(dllexport) 函数名()定义的输出函数, 在C#中调用时, 如前文所述, 使用 [DllImport("D:\VS2005P ...
- C#调用C++编写的DLL函数, 以及各种类型的参数传递 z
1. 如果函数只有传入参数,比如: C/C++ Code Copy Code To Clipboard //C++中的输出函数 int__declspec(dllexport) test(consti ...
随机推荐
- Ubuntu系统安装搜狗拼音输入法
问题背景 Ubuntu是使用人数最多的Linux发行版之一,其丰富的生态广受各位种花家程序员小伙伴们的欢迎.对种花家小伙伴们来说,最重要的功能之一就是中文输入法了.但是Ubuntu自带的中文输入法效果 ...
- 以MPU6050为例的硬件IIC的使用
目录 参考调试MPU6050与EEPROM的经验,整合了目标内存/寄存器地址是否为16位的情况,合并了单字节与多字节间的操作,添加了返回值与读写超时功能:硬件IIC的7位从机地址查询方式读写参考代码 ...
- LWC-002_Composition
Composition (lwc.dev) Set a Property on a Child Component # Send an Event from a Child to an Owner ...
- wendows 批量修改文件后缀(含递归下级)
for /r %%a in (*.jpg)do ren "%%a" "%%~na.png" //-- or :for /r %a in (*.jpg)do re ...
- HTTP头注入:XFF注入
0x00:XFF是什么? X-Forwarded-For:简称XFF头,它代表客户端,也就是HTTP的请求端真实的IP,(通常一些网站的防注入功能会记录请求端真实IP地址并写入数据库or某文件[通过修 ...
- React函数式组件值之useRef()和useImperativeHandle()
一.useRef useRef共有两种用法,获取子组件的实例(只有类组件可用),在函数组件中的一个全局变量,不会因为重复 render 重复申明, 类似于类组件的 this.xxx. 1. useRe ...
- 达梦数据库manager工具坑
领导突然说要用达梦的数据库,对此完全没有了解. 安装没有问题. 但是这个工具真的坑到了. 因为之前用的都是navicat 干mysql ,创建数据的时候默认执行了. 但是这个工具tool/manage ...
- 刘蓉年谱.PDF
书本详情 刘蓉年谱.PDF 所有责任者: 陆宝千著 所有题名: 并列正题名:A chronological biography of Liu JungChronological biography o ...
- appium遇到的问题
问题1:权限问题:java.lang.SecurityException: Injecting to another application requires INJECT_EV ENTS permi ...
- Android版本功能
Android 1.0 2008年9月23日,发布Android操作系统中的第一个正式版本:Android 1.0(Astro"铁臂阿童木").全球第一台Android设备HTC ...