1. Early-Z
总结:Z-Test在逐片元操作的Blending阶段进行,Early-Z提前进行了,在光栅化阶段的片元着色器之前进行。
如果Early-Z深度测试失败,就不必进行片元着色器阶段的计算了。
深度测试回顾
数值小的通过测试:
--->
1.1. 定义
其中的逐片元操作详细流程:
在传统的渲染管线中,ZTest是在逐片元操作的Blending阶段进行的。其中的一部分像素不会通过ZTest,它们仍然在进行深度测试之前还需要执行前面的一系列操作,这会造成性能的浪费,而Over Draw也是这么产生的。
因此现代GPU中运用了Early-Z的技术,大致的渲染管线可以描述成:
- 应用阶段(CPU)->几何阶段(顶点着色器)->early-z(提前深度测试)->光栅化阶段的片元着色器->各种测试(深度测试,透明度测试,模板测试等)->颜色缓冲区(buffer)
如果Early-Z深度测试失败,就不必进行片元阶段的计算了,因此在性能上会有很大的提升。
但是最终的ZTest仍然需要进行,以保证最终的遮挡关系结果正确。
- 前面的一次主要是Z-Cull,为了裁剪以达到优化的目的
- 后一次主要是Z-Check,为了检查,如下图:
1.2. Early Z失效的情况
- 开启Alpha Test 或 clip/discard 等手动丢弃片元操作(理解:Early Z会先于它们执行,那么只有离摄像机最近的保留下来了,其他的被剔除掉了,但是可能保留下来的有透明物体或者clip掉的洞,那么远处的物体不应该被剔除掉,是可以看见的)
透明度测试(AlphaTest):只要一个片元的透明度不满足条件(通常是小于某个阙值),那么它对应的片元就会被舍弃,不进行进行深度测试、深度写入等。否则就不透明, 进行深度测试、深度写入等。透明度测试是不需要关闭深度写入。产生的效果很极端,要么完全透明,要么完全不透明。
例如,A是透明物体,B不透明。
- 先执行Early Z,写入A的深度。剔除B的紫色部分,不会执行片段着色器。
- 再执行透明度测试,A透明,会被丢弃,不会写入A的深度值(但是实际上Early-Z的时候已经写入了)。
- 这样的话A和B都没了。
- 手动修改GPU插值得到的深度,与上面类似
- 开启Alpha Blend
开启透明度混合的话需要ZWrite Off,那么不会写入深度值,Early Z阶段也无法进行深度写入,冲突了。
- 关闭深度测试Depth Test
1.3. 高效利用Early Z技术
如果按照3-2-1顺序渲染测试,那么他们都会通过测试,这样的话Early-Z将不会带来任何优化效果。
如果按照1-2-3顺序渲染测试,那么Early-Z的优化效果将达到最大。
因此在渲染前,将不透明物体从近往远渲染的话,Early-Z能发挥最大的性能优化。
如何实现呢?可以让cpu将物体按照由近到远的排序,再交付给gpu进行渲染。
但是在复杂的场景中频繁的排序,cpu性能消耗会很大。此外,严格按照由近到远的顺序渲染,将不能同时搭配批处理的优化手段。
有没有其他方法? 此时就引申出了pre-z 技术。
2. PreZ(Z-Prepass)
2.1. 定义
Z-PrePass使用了两个Pass:
- PrePass:第一个Pass,仅开启深度写入,不输出任何颜色信息。
- BasePass:第二个Pass,关闭深度写入,并且将深度比较函数设置为Equal,并且渲染颜色。
2.2. 优化
但是这样显然多了一个pass的消耗,会出现两倍的drawcall。并且多pass的shader无法进行动态批处理。
解决方案:仍然使用两个pass,但多个shader
- Prepass:单独分离出一个shader,并用这个shader将场景的不透明物体先渲染一遍
- BasePass:仍然关闭深度写入,深度比较函数仍然为相等,进行正常的透明度混合
注:URP的SRP batch做的合批是不会减少Draw Call的
- 它的最大的优化在于合并set pass call,减少set pass call的开销。
- 因为CPU上的最大开销来自于准备工作(设置工作),而非DrawCall本身(这只是要放置GPU命令缓冲区的一些字节而已),因此draw call是不会减少的。
2.3. Pre-Z用于透明渲染
Pre-Z也是透明渲染的一种解决方案,例如下图单个Pass不写入深度情况下的透明渲染,会出现透明物体交叠的情况。
而采用PreZ会使用两个Pass,分别写入深度和进行透明度混合。
但是这样会存在一个问题:无法看到透明物体的背面。解决方法是将渲染分为正面背面两部分:
- pass1只渲染背面(cull front)
- pass2只渲染正面(cull back)
由于Unity会顺序执行Subshader中的各个Pass,所以我们可以保证背面总是在正面被渲染之前渲染,来得到正确的深度渲染关系。
2.4. PreZ使用建议
那PreZ有性能消耗,是否采用呢?
一项性能测试实验得出:
可以看到,PreZ的消耗为2.0ms,而带来的几何变换光栅的优化只减少了0.3ms(2.7-2.4)。
但在实际情况中,如果一个场景OverDraw很多,且不能很好的将不透明物体从前往后进行排序时,此时PreZ的性能消耗是远小于Overdraw带来的消耗的,因此可以考虑使用PreZ进行优化。
因此PreZ是需要根据项目的实际情况来决定是否采用的。且时刻注意PreZ会增加DrawCall,如果用错了可能是负优化。
3. 示例-多边形头发渲染
参考:
HairRendering.pdf (oregonstate.edu)
头发的渲染耗时量大,因为它的数量很多,并且有各种各样的发型。例如在《最终幻想:灵魂深处》中头发的渲染占据了25%的时间。因此为了节约消耗,当今游戏界所大量采用的做法是头发的多边形建模。
头发建模可分为发丝建模与多边形建模两种。
- 多边形建模有更低的几何复杂性,以至于有更高的排序效率;相比之下采用发丝建模需要大约100K-150K的发丝来构建,复杂度高很多;
- 采用多边形建模可以更加容易的集成到已有的渲染管线中去,基本已有的渲染管线都是处理的多边形模型;
头发生成过程:
3.1. 建模
建模头发面片,由一层层一定体积的面片组成头发。
3.2. 增加头发纹理
基础纹理Base texture:拉伸的噪声纹理
透明度纹理Alpha texture:应该有完全不透明的区域
高光偏移纹理Specular shift texture
高光噪声纹理Specular noise texture
3.3. 着色
3.3.1. Kajiya-Kay Model
Kajiya-Kay Model (卡吉雅模型)是各向异性的strand lighting 模型。它在光照公式中使用发丝切线T(副切线)而不是法线,并且选择的法线 N 位于 T 和光照方向 L 组成的平面上,其中H为半程向量,L为光线方向。
卡吉雅模型本质是选择一个法向量:每根发丝都有无数条法线,我们应该选取哪条呢。我们发现垂直于法线的另外一个向量切线是唯一的。过切线的起点并且与切线和光照方向共面,可以找到唯一的一条法线,我们使用这条法线就可以计算出一个近似的高光。
所以公式可以改写成如下所示:
但是如果按照上述方式找到一条法线会产生一个问题,也就是似乎高光位置跟视线V没有关系。也就是随着视线移动,“天使环”高光带不会跟着移动。为了解决这个问题,卡吉雅模型模仿Blinn-Phong使用半程向量H,这样“天使环”位置就会同时受平行光和视线影响了。左边为Bllin-Phong高光计算公式,卡吉雅模型改进为右边的公式:
代码如下:
float KajiyaSpecular(float3 T, float3 V, float3 L,float specularity)
{
float3 H = normalize(L+ V);//半程向量
float dotTH= dot(T, H);
float sinTH= sqrt(1.0 -dotTH*dotTH);
float dirAtten= smoothstep(-1.0, 0.0, dot(T, H));
return dirAtten* pow(sinTH, specularity);
}
dirAtten
为衰减系数,它控制着你可以看到的照明范围。
smoothstep(min,max,a)
控制a在min和max之间平缓变化。
此外根据头发散射的特性,我们可以观察到头发的高光:
- 头发有两层高光;
- 主高光流向发尾;
- 次高光透出一点头发的颜色,且流向发根;
- 次高光带不是很连续;
shader编写思路:
- 顶点着色器:传输切线,法线,视线方向,光照方向和全局光照
- 片元着色器:
- 漫反射计算
-
- Kajiya-Kay漫反射分量
sin(T,L)
在没有阴影的情况下太亮了,所以我们使用的是调整过的dot(N,L)
- Kajiya-Kay漫反射分量
- 两种偏移高光计算
- 结合,传输出最终的颜色信息
3.3.2. 偏移高光
如何模拟高光的切变流向,即一个位置偏向发梢,一个偏向发根。由于我们使用模型的切线来计算高光,要想改变高光位置,只能从切线T下手。AMD提供的方法为,使用N(这里是多边形几何的法线,不是法线平面中的法线N1)对T进行偏移;偏移量可以从贴图中进行采样,计算公式如下:
float ShiftTangent(float3 T, float3 N, float shift)
{
float3 shiftedT = T + shift * N;
return normalize(shiftedT);
}
如下图:T'与T''是偏移后的切向量;
从高光偏移纹理中采样出偏移值shift,丰富头发细节:
3.3.3. Specular Strand Lighting
根据kajiya模型来计算。使用半程向量计算减少计算复杂度。两条高光有两种不同的颜色、切线偏移和高光指数。因此需要通过噪声纹理调整两次高光:
代码和之前一样:
float StrandSpecular(float3 T, float3 V, float3 L,float specularity)
{
float3 H = normalize(L+ V);//半程向量
float dotTH= dot(T, H);
float sinTH= sqrt(1.0 -dotTH*dotTH);
float dirAtten= smoothstep(-1.0, 0.0, dot(T, H));
return dirAtten* pow(sinTH, specularity);
}
3.3.4. 合并
合并到一起:
float4 HairLighting (float3 tangent, float3 normal, float3 lightVec,
float3 viewVec, float2 uv, float ambOcc)
{
// shift tangents
float shiftTex = tex2D(tSpecShift, uv) - 0.5;
float3 t1 = ShiftTangent(tangent, normal, primaryShift + shiftTex);
float3 t2 = ShiftTangent(tangent, normal, secondaryShift + shiftTex);
// diffuse lighting
float3 diffuse = saturate(lerp(0.25, 1.0, dot(normal, lightVec)));
// specular lighting
float3 specular = specularColor1 * StrandSpecular(t1, viewVec, lightVec, specExp1);
// add second specular term
float specMask = tex2D(tSpecMask, uv);
specular += specularColor2 * specMask * StrandSpecular(t2, viewVec, lightVec, specExp2);
// Final color
float4 o;
o.rgb = (diffuse + specular) * tex2D(tBase, uv) * lightColor;
o.rgb *= ambOcc;
o.a = tex2D(tAlpha, uv);
return o;
}
实现过程:
不同模型实现效果对比:
3.4. 近似深度排序
需要从后往前的顺序计算正确的透明度混合,对于头发来说从里至外的透明度混合也是类似的。
使用固定顺序,先计算里层头发,再计算外层头发,并且在Preprocess中计算(对头发整个面片进行排序而不是对单独的三角形排序)。
渲染过程:分为3个pass
- pass1
- 处理不透明部分,开启Alpha test透明度测试,仅通过不透明的像素,
- 关闭背面剔除
- 开启深度写入
- pass2
- 剔除正面,渲染背面
- pass3
- 剔除背面,渲染正面
问题:会带来非常多OverDraw的问题
3.5. 优化方案
- Pass1:用不透明毛发区域的深度填充深度缓冲区
- 启用 Alpha 测试,只通过不透明像素
- 关闭背面剔除
- 启用深度写入,将深度测试设置为 Less
- 关闭颜色缓冲区写入
- 使用仅返回透明度的简单片元着色器
- Early Z在此过程中没有任何作用,但该Pass性能消耗很小
- Pass2:渲染不透明区域
- 开始使用全头发片元着色器
- 禁用背面剔除
- 关闭深度写入
- 将深度测试设置为 Equal- 只有在第 1 次测试中写入深度的片段才能通过深度测试,即不透明区域
- 该测试及后续测试无需进行透明度测试,从而受益于Early Z
- Pass3:渲染背面半透明部分
- 剔除正面
- 关闭深度写入 - 深度顺序不一定正确
- 将深度测试设置为Less
- Pass4:渲染正面透明部分
- 剔除背面
- 启用深度写入
- 将深度测试设置为Less
- 启用深度写入可防止深度顺序错误,但可能会牺牲过多的剔除量
- 掩盖上一次处理中可能出现的深度顺序缺陷Pass
3.6. 总结
优点
- 几何复杂度低
- 减少顶点引擎的负荷
- 深度排序更快
- 便于低端硬件使用
缺点
- 没有动画效果
-
- 悬垂的马尾等需要单独处理
- 在运行时对物体进行排序可解决这一问题
- 不适用于所有发型
总结:
- 艺术资产
-
- 多边形头发模型
- 材质
- 着色
-
- 漫反射
- 两个镜面反射
- 环境光遮蔽
- 近似深度排序
- Early Z优化