OIT即为在采用光栅化流程的不经过透明物体远近排序情况下的透明度渲染算法。
其存在的必要性为,在传统光栅化渲染半透明物体时,往往渲染的顺序十分重要,传统的 bold{over} 算子决定了在blend的过程中,从后往前叠加才能正确得到最终效果。
那么由于需要确定渲染顺序,那么排序便必不可少,然而排序操作又十分消耗性能,且对于某些自相交、循环遮挡的透明物体难以处理,因此避免排序的诸多算法便应运而生。比较常见的有A-buffers、 text Z^3 、Multi-Layer Alpha Blending、k-buffers、Depth Peeling以及Weighted average等算法。
回顾 bold{over} 和 bold{under} 两种混合算子:
bold{over}:bold c_o = alpha_sbold c_s+(1-alpha_s)bold c_s/ / bold{under}:bold c_o = alpha_dbold c_d+(1-alpha_d)alpha_sbold c_s,/ bold a_o = alpha_s(1-alpha_d)+alpha_d=alpha_s-alpha_salpha_d+alpha_d.
1984年Loren Carpenter的论文中首次提及A-buffer算法。
A-Accumulation,顾名思义,其为一个累加buffer。
A-buffer中采用链表进行数据存储。
在之后的与透明度有关的A-buffer算法中,每个晶格将会被指定coverage mask,在之后的渲染过程中,每碰到一个物体片断其对应的coverage信息都会被记录下来,而这些mask遮挡的像素点将会根据这些片断的深度将这些相应的coverage信息链入相应像素的A-buffer中,因其频繁的插入操作也可以知晓为何其采用链式存储。
尽管需要的空间更多了,但是其对于透明物体的渲染混合问题得到了有效的改善。
在物体遍历之后,每个像素的A-buffer被用来记录所有覆蓋这个像素的片断的信息。在最后对其进行混合处理。
这种方法不仅可以用于OIT,对于抗锯齿也有一定的效果。
A-buffer 对于穿插的物体渲染仍然存在问题,如下图。
可以看到,在当前像素点真正的先后顺序关系可能会因为采样点与表面交叠的模样而变化。
且A-buffer容量的unboundness特性也使得A-buffer的实际运用不是非常合适。
Z^3 算法将子像素分组为多个片断,它们有着X、Y、Z的梯度以及Z值。因为以一个字节的浮点数来存储这些值,故此方法内存需求不比SSAA。
在coverage mask中存储著大量采样点,当其存储复杂度超过允许的存储空间,那么拥有最接近Z值的片断将会被合并。
首先是 Z^3 算法使用的数据结构:
相比起为每个采样点单独提供颜色、z、模板测试值,在该算法仅为每个像素提供几个片断实体。每个片断有m位的coverage mask来表示这些采样点被覆蓋的信息。在每个片断实体中,其颜色为被覆蓋的采样点的颜色的平均,其z值以像素中心值为准,保留其X、Y、Z梯度以利用于准确地知道采样点位置。
综合而言,该算法在每个像素单位使用一个固定大小的存储空间,沿用A-buffer思路,在存储信息将要溢出空间时将最近z值的两个片断进行合并来达到一个高效以及内存友好的解决方案。因此其在OIT方面也算是奏效。
但其缺点在于,倘若两个片断被merge之后,新加入的在此两片断之间的第三片断就无法插入。
例如某像素最多可以容纳2个片断信息,但不巧的是刚好深度最前与最后的两个被写入了,假设此时加入新的片断,根据算法,将会merge或是最前或是最后那个片断,那么倘若又有新的片断加入,其深度位于先前被merge的两个片断之间,那么此片断写入便无法按照算法达到最优混合了。
A-buffer存在的问题就是其大小未知,在一些复杂场景中A-buffer空间占用可能会十分巨大。
而Multi-Layer Alpha Blending(MLAB)的解决方案为以组进行混合。
与A-buffer类似,MLAB的核心思想在预先合并一些深度相近的fragment作为一个layer,再最终合成时再将这些layer合并。
其核心想法即为分组无序blend,然后按组合并。
k-buffer与a-buffer一样可以被看作像素中一系列允许读取、修改和写入的fragment的容器。其允许流式地将这些fragment进行比较、排序、混合以及丢弃等操作。许多需要多pass才可以实现的效果仅需要利用k-buffer然后使用一个pass即可完成。
传统的z-buffer based的帧缓存仅在每个像素单位存储一个深度值,a-buffer则又是不限具体大小的,k-buffer则允许存储至多k个,因此我们可以利用该k-buffer进行高效的depth peeling等。
借助Read-Modify-Write(RMW)特性,该算法可以表述为:
1.读取该像素的k-buffer中的数据以及新加入的片断f。
2.使用片断f对k-buffer中的元素进行修改。
3.将k-buffer中的元素写回,并丢弃片断f。
其中,步骤二的“修改”可以有多种方法,视具体的效果而定。
Depth Peeling中文称为深度剥离算法。它隐式地将表面进行了排序。
可以选择利用介绍的k-buffer进行操作,它的工作原理为:
1.将待渲染场景最前方片断进行渲染。
2.寻找场景第二前方的片断进行渲染。
…
n.从后往前合成场景
但由于其剥离层数以及多pass等限制,还有很大的优化空间。
双向深度剥离是基于Depth Peeling的一个改进算法。
相比于普通的Depth Peeling,它在每个geometry pass中从前以及从后同时进行剥离。将pass数目从N降低到了N/2+1的数目。其在GPU带有多重深度缓存信息并且各自关联不同texture的情况下很容易实现。
自适应透明度算法的近似算法依然是对每个像素采用一个固定的存储空间。
有点类似于 Z^3 算法,它也在存储的fragment信息将要溢出空间时进行一些“刷新”操作。
但其并非在溢出时合并某些fragment,而是移除某些对该像素最终颜色贡献较小的fragment。
这种方法保证了最终渲染出来的图像误差项仅为那些对该像素贡献较小的透明颜色片断。
随机透明度算法则通过计算alpha遮蔽概率来在multi-sampled纹理上来进行处理。
类似于screen-door算法,但其在采样点层级进行随机填充覆蓋。
在采样点足够多的时候,alpha-to-coverage覆蓋效果越好,因其覆蓋的比率越接近其真实alpha。
但在采样点不足的情况下,这种方法将产生很严重的噪声。
首先介绍Weighted Sum算法:
以 bold{over} 算子计算,令 C_n,A_n,D_0 分别为第n层的颜色、第n层的alpha值以及背景色。那么假设有四层,计算将为:
将D_1,D_2,D_3带入D4中,可以得到:
重新整理
可以看出其有一定的规律,在此我们将要探究这些算式是否有可优化之处。
将其分为order-dependence与order-independence两部分:
其中order-dependent的依据为当前项各乘数之间是否为轮换对称的。
那么文章中提出了两点优化:
1.对于乘积过长的部分,由于其最终贡献较小,因此可以选择忽略不计。
2.在alpha值都相对较低时,order dependent 部分可以忽略不计。
因此,该算法最终有
相比原先的3pass,3rt而言,该方案降低为仅需要2pass以及单rt,但其在alpha较高时的错误十分明显。
其算法的核心思想即为对逐层over时的一些算式优化,舍弃低贡献度片断以及相对低alpha时的order dependent部分舍弃。
其公式:
C_{dst}=sumleft[A_{src}C_{src}right]+C_{bg}(1-sum A_{src})
Weighted Average方法是在Nvidia的Dual Depth Peeling中采取的一种对Weighted Sum出现的在alpha较高时出现视觉错误的局限性的优化方法。
对于不同颜色值的近乎不透明物体,在混合时采用其alpha值作为权重来混合得出颜色。
alpha_{target} = frac{sum^n_{i=1}alpha_i}{n}
仅需一次物体渲染pass加以一次全屏后处理即可。
在Weighted Blended Order-Independent Transparency中,将z值也作为权重的一部分加以计算混合透明度值,z值越大则权重越小。
Hybrid Transparency(HT)也是一种OIT算法,避免了排序的问题以及改善物体重叠交叉的效果。
在带有透明物体的场景渲染中,我们可以发现,若观测角度某个物体在一个透明物体之下,那么其将会变暗,而变暗的程度与其被遮挡物体的alpha以及被遮挡物体数目有关。
在物体数目叠加达到一定程度之后,可以认为该物体对于整个像素的颜色贡献微乎其微。
因此我们可以将透明层分为:
HT采用的方法为:以精确的方法计算k层核心层的颜色,然后对剩余n-k层附加层的颜色进行一个十分快速的估计计算。
这两种方法都是order-independent的,但只有核心层需要进行排序。
该HT算法主要有以下优势:
对于一个片断,其可见性由两部分组成:
1.其不透明度
2.对应像素位置之前的片断累加透射程度
关于core与tail层的分类与Transmittance Function的近似关系如下:
为了提取出前k个层作为core层,我们使用截断A-buffer作为存储工具(truncated A-buffer,A.K.A k-TAB)
然后使用 bold{over} 算子计算出core层fragment的可见性,其计算如下:
v_i=begin{cases}alpha_1&,i=1/alpha_itimes(1-sum_{j=1}^{i-1}v_j)&,i>1end{cases}
然后通过以下式子来计算其颜色与alpha值:
C_{core}=sum_{i=1}^k(C_itimes v_i)/alpha_{core}=sum_{i=1}^kv_i.
计算完core层的数据之后溢出的剩余n-k层则用于计算tail层。
由于tail层既要保证内存不溢出且fragment数目也并非可控,因此我们可以选择采用Weighted Sum或Weighted Average算法进行tail层的透明度估计计算。
此处采用Weighted Alpha Average的方法,使用以下两个公式计算:
C_{acc}=sum_{i=k+1}^n(C_itimesalpha_i) alpha_{acc}=sum_{i=k+1}^nalpha_i
因此,计算tail层:
C_{tail}=frac{C_{acc}}{alpha_{acc}}./alpha_{avg}=frac{alpha_{acc}}{n-k}.
那么tail层的透射为:
tau_{tail}=(1-alpha_{avg})^{n-k}
则其alpha为:
alpha_{tail}=1-tau_{tail}.
此时我们需要将Core层、Tail层与Background进行blend。
根据定义,core层的transmittance对tail层与background的混合有着限制,而background的可见性也同时有被tail层限制,三者限制关系逐个叠加。
最终的颜色将考虑以下三个部分:
因此我们易得:
C_{final}=C_{core}+(1-alpha_{core})times(C_{tail}+(1-alpha_{tail})times C_{bg}).
Hybrid Transparency方法需要两次geometry pass,第一次geometry pass提取k个最近fragment,第二次geometry pass中进行着色与分层。最后的全屏pass将这三个layer(core、tail、background)进行合并。
第一个pass收集前k个layer并将其以3d纹理的形式存储,即k-TAB(k-length truncated A-buffer),由于A-buffer链式存储的特性,因此插入操作仅需对其进行bubble sort操作,因此在一个pass内即可完成。现代GPU不提供超过32位字的原子操作,但这些操作足够容纳每个片断编码24位的深度与8位的透明度。
在第一个pass之后我们已经有了全屏范围的几何图元,因此我们可以生成对应的逐像素的可见性函数(可见性函数可参考上文中阶梯状折线图)。
生成可见性函数的操作为,对k-buffer进行一次遍历,然后将逐深度可见性生成为另一张存储k深度的3d纹理。
在第二个geometry pass中,执行对片断的着色操作,然后依据其深度转发到对应的处理流程(core or tail)中,最终合成到对应的color buffer中。其中,转发判断依据为判断当前片断的深度是否出现在k-TAB中。
最后将不同color buffer的值与背景进行混合。
原文中的算法流程:
(ps:过几天把实现贴上来)
Author:Randyczhang