技术美术百人计划 | 《3.5 Early Z与Pre Z》笔记

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失效的情况

  1. 开启Alpha Test 或 clip/discard 等手动丢弃片元操作(理解:Early Z会先于它们执行,那么只有离摄像机最近的保留下来了,其他的被剔除掉了,但是可能保留下来的有透明物体或者clip掉的洞,那么远处的物体不应该被剔除掉,是可以看见的)

透明度测试(AlphaTest):只要一个片元的透明度不满足条件(通常是小于某个阙值),那么它对应的片元就会被舍弃,不进行进行深度测试、深度写入等。否则就不透明, 进行深度测试、深度写入等。透明度测试是不需要关闭深度写入。产生的效果很极端,要么完全透明,要么完全不透明。

例如,A是透明物体,B不透明。

  • 先执行Early Z,写入A的深度。剔除B的紫色部分,不会执行片段着色器。
  • 再执行透明度测试,A透明,会被丢弃,不会写入A的深度值(但是实际上Early-Z的时候已经写入了)。
  • 这样的话A和B都没了。

  1. 手动修改GPU插值得到的深度,与上面类似
  2. 开启Alpha Blend

开启透明度混合的话需要ZWrite Off,那么不会写入深度值,Early Z阶段也无法进行深度写入,冲突了。

  1. 关闭深度测试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%的时间。因此为了节约消耗,当今游戏界所大量采用的做法是头发的多边形建模。

头发建模可分为发丝建模与多边形建模两种。

  1. 多边形建模有更低的几何复杂性,以至于有更高的排序效率;相比之下采用发丝建模需要大约100K-150K的发丝来构建,复杂度高很多;
  2. 采用多边形建模可以更加容易的集成到已有的渲染管线中去,基本已有的渲染管线都是处理的多边形模型;

头发生成过程:

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之间平缓变化。


此外根据头发散射的特性,我们可以观察到头发的高光:

  1. 头发有两层高光;
  2. 主高光流向发尾;
  3. 次高光透出一点头发的颜色,且流向发根;
  4. 次高光带不是很连续;


shader编写思路:

  1. 顶点着色器:传输切线,法线,视线方向,光照方向和全局光照
  2. 片元着色器:
  • 漫反射计算
    • Kajiya-Kay漫反射分量sin(T,L)在没有阴影的情况下太亮了,所以我们使用的是调整过的dot(N,L)
  • 两种偏移高光计算
  • 结合,传输出最终的颜色信息

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

  1. pass1
  • 处理不透明部分,开启Alpha test透明度测试,仅通过不透明的像素,
  • 关闭背面剔除
  • 开启深度写入
  1. pass2
  • 剔除正面,渲染背面
  1. pass3
  • 剔除背面,渲染正面

问题:会带来非常多OverDraw的问题

3.5. 优化方案

  1. Pass1:用不透明毛发区域的深度填充深度缓冲区
  • 启用 Alpha 测试,只通过不透明像素
  • 关闭背面剔除
  • 启用深度写入,将深度测试设置为 Less
  • 关闭颜色缓冲区写入
  • 使用仅返回透明度的简单片元着色器
  • Early Z在此过程中没有任何作用,但该Pass性能消耗很小
  1. Pass2:渲染不透明区域
  • 开始使用全头发片元着色器
  • 禁用背面剔除
  • 关闭深度写入
  • 将深度测试设置为 Equal- 只有在第 1 次测试中写入深度的片段才能通过深度测试,即不透明区域
  • 该测试及后续测试无需进行透明度测试,从而受益于Early Z

  1. Pass3:渲染背面半透明部分
  • 剔除正面
  • 关闭深度写入 - 深度顺序不一定正确
  • 将深度测试设置为Less

  1. Pass4:渲染正面透明部分
  • 剔除背面
  • 启用深度写入
  • 将深度测试设置为Less
  • 启用深度写入可防止深度顺序错误,但可能会牺牲过多的剔除量
  • 掩盖上一次处理中可能出现的深度顺序缺陷Pass

3.6. 总结

优点

  • 几何复杂度低
  • 减少顶点引擎的负荷
  • 深度排序更快
  • 便于低端硬件使用

缺点

  • 没有动画效果
    • 悬垂的马尾等需要单独处理
    • 在运行时对物体进行排序可解决这一问题
  • 不适用于所有发型

总结:

  • 艺术资产
    • 多边形头发模型
    • 材质
  • 着色
    • 漫反射
    • 两个镜面反射
    • 环境光遮蔽
  • 近似深度排序
  • Early Z优化

  • 18
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值