【Unity Graphics】 01_光

1 渲染路径Rendering Path

1.1 梗概

1.1.1什么是渲染路径?

要理解渲染路径,可能从渲染管线说起较好。所谓渲染管线(Render Pipeline),就是指将场景中的三维模型转换成屏幕上的二维图像的一系列操作。Unity起初只有一种内置渲染管线即Built-in Render Pipeline(《Unity Shader入门精要》一书中使用的就是内置渲染管线),但内置渲染管线有两大缺点:①不可更改不灵活,②代码臃肿性能低。因此Unity后来新增了一种可编程渲染管线即Scriptable Render Pipeline (SRP)(这里的“可编程”是相对于内置渲染管线而言的,新的渲染管线更具有灵活性;但内置渲染管线本身也被称作“可编程”的,这是相对于过去的固定函数渲染管线而言的,在固定函数渲染管线中着色算法都是被写死的),一方面它可以让我们在C#脚本中通过调用API来调整渲染流程,另一方面它使用了SRP Batcher技术来减少CPU在渲染状态设置上的开销(SRP Batcher会预先将所有模型数据和材质数据永久存储到GPU中,只要这些数据不变动,CPU只需要在每帧渲染时对模型数据和材质数据进行绑定即可,而无需重新上传数据,因此可减少GPU的设置开销但不会减少Draw Call数量)。由于自定义SRP难度较大,Unity还为我们提供了两种SRP模板,即通用渲染管线Universal Render Pipeline (URP) 和高清渲染管线High Definition Render Pipeline (HDRP),前者可以理解为内置渲染管线的一个进化版,后者则更多面向高端设备。

而渲染路径则是渲染管线中一系列和光照着色相关的操作(关心的操作是渲染管线的一个子集),对光照的不同处理被视作不同的渲染路径。Unity中较为常用的有两种渲染路径:前向渲染路径Forward Rendering Path和延迟渲染路径Deferred Rendering Path。同一种渲染路径在不同渲染管线中的具体设置是不同的,比如,前向渲染在内置渲染管线中要经过多个pass的光照处理(对每一个重要光源执行一遍pass),但在通用渲染管线中只会经过一个pass的光照处理(在一个pass中遍历所有光源,这也限制了可用光源的数量)。

后文谈到的前向渲染和延迟渲染指的都是内置渲染管线中的渲染路径,对其他渲染管线的渲染路径后续再补充

 

1.1.2有哪些渲染路径?

正如上文所说,常用的渲染路径有前向渲染和延迟渲染两种。我们都知道,一条经典而抽象的渲染管线大致分为顶点阶段、光栅化、片元阶段和逐像素处理四个步骤,在顶点阶段或片元阶段计算的光照着色信息可能会在逐像素阶段的深度测试中被丢弃掉。而前向渲染就是顺应这个顺序、在深度测试前先计算光照着色的渲染方式,延迟渲染就是指违反这个顺序、在深度测试后再计算光照着色的渲染方式。具体来讲,延迟渲染会先使用一个不做任何着色计算的pass进行深度测试,并将最终可见的片元的颜色、法线等信息储存起来(G-Buffer),在这之后再用另一个pass来计算光照着色,而这后一个pass仅处理一个贴合在摄像机近裁剪面上的矩形而不会再次渲染整个场景(类似于后处理操作)。因此,无论光源数量如何增长,延迟渲染的性能消耗都不会增加太多,但延迟渲染的功能不如前向渲染丰富,最明显的是它无法处理透明物体。

这里顺带区分一下几种解决过度绘制的技术,和延迟渲染对比着看会获得更清晰的理解:

①    early-z:一种硬件解决方案,会在光栅化和片元阶段之间额外增设一个深度测试阶段。但要注意,在处理透明物体时,early-z计算得到的深度信息是不正确的,因此需要late-z重新计算,反而会消耗性能,所以通常gpu会在这种情况下关闭early-z。此外,early-z的优化效果并不稳定(想象一种所有片元从后向前绘制的情况)。

②    z-perpass:一种软件解决方案,会使用两个pass,并结合early-z技术。第一个pass仅写入深度信息,不做任何着色计算;第二个pass关闭深度写入,并将深度比较函数设为“相等”,再进行着色计算。这种技术可以解决early-z效果不稳定的问题。

 

1.1.3怎么用渲染路径?

可以在project settings中为整个项目设置渲染路径,也可以对摄像机单独设置渲染路径以覆盖项目设置。而当写shader时,我们需要为pass指明渲染路径标签(前向渲染需要设置LightMode为ForwardBase或ForwardAdd,延迟渲染需要设置LightMode为Deferred),这是为了和Unity的底层引擎进行交流从而获得正确的与光照相关的变量。但说到底,所谓渲染路径只是Unity认为的一种比较合理的光照处理模式,并为我们提供了一个基本框架而已,我们完全可以打破这些规定,比如在ForwardAdd中进行逐顶点光照而非逐像素光照,就如同我们可以对渲染管线进行自定义配置一样。

 

 

 

1.2前向渲染路径 Forward Rendering Path

1.2.1梗概

核心思想是用两个pass来处理不同的光源。模式为ForwardAdd的pass执行多次,每次逐像素地处理一个重要光源;模式为ForwardBase的pass执行一次,处理其余所有光源。

起初我很不理解,为什么要有ForwardAdd这样耗费性能的设计,在一个pass中遍历所有光源不是更省性能吗?后来了解到,URP中的前向渲染就是这么做的,但这也带来一个问题:每次Draw Call时GPU能接收的参数是有限的,因此可用光源的数量就遭到了限制,反观内置渲染管线中的前向渲染却没有这个问题。此外,ForwardAdd还有一个好处,就是对shader编写者来说节省了工作量,无论光源数量怎么增加,我们都仅需要写一遍代码,然后将重复工作交给Unity引擎即可。有了这个意识,前向渲染路径就很好理解了,既然ForwardAdd负责重复处理那些重要光源,那将剩余的不重要光源都交给ForwardBase一次性处理就好了,顺带再挪一个逐像素光源过去,还可以省下一个ForwardAdd的pass。因此,ForwardBase要处理的内容可以被大体分为两块:①一个逐像素光源,②所有需要一次性处理的东西。

以下是对两种pass要点的梳理。

 

1.2.2 ForwardBase

①    逐像素地处理最亮的平行光,并默认开启阴影。

②    处理所有逐顶点光源和球谐函数光源(对于一个空间点附近的球面区域上的光照信息,用真实的光照公式计算几个采样点的值,拟合出近似的光照函数F(x)后,对若干球谐函数基底f(x)进行积分以求出系数k,这些系数可以用来快速重建该球面区域上的光照信息),逐顶点的点光源最多为4个。

③    计算自发光和环境光。

④    需要使用#pragma multi_compile_fwdbase预编译指令,生成着色器变体。

1.2.3 ForwardAdd

①    逐像素地处理重要光源,上限数量可在project settings中设置。

②    默认不开启阴影,如需要开启可使用#pragma multi_compile_fwdadd_fullshadows预编译指令。

③    要开启混合模式,通常使用Blend One One.

 

*注意:不同光源组之间存在重叠,逐像素处理的最后一个光源同时也是逐顶点处理的,逐顶点处理的最后一个光源同时也是按球谐光照处理的。文档中解释说,这是为了能减少当物体或光源移动时的“光照跳跃”现象,我大概能理解这是什么意思,但无法对“光照跳跃”有一个很直观的印象,待后面补充相关信息

 

1.3延迟渲染路径 Deferred Rendering Path

关于什么是延迟渲染,上文已有详细描述。这里梳理几个要点:

①    延迟渲染不支持透明效果。因为MRT中只记录了离摄像机最近的片元的信息,那么对需要看起来透明的像素而言,它背后应当被呈现出来的画面就无法计算得到了。

②    延迟渲染不支持MSAA。这不是说在理论上做不到延迟渲染和MSAA的结合,而是在实际使用中我们通常不采纳这种做法。因为MSAA的本质就是对同一个像素记录并整合多份采样数据(比如4xMSAA会在遍历一个三角形时对同一个像素进行4次采样,每个采样点都会有一个coverage mask来记录它是否被三角形覆盖,每个采样点也都会经历一遍深度测试。如果一个采样点既被三角形覆盖,又通过了深度测试,那么片元着色器中计算得到的片元颜色就会被写入该采样点对应的颜色缓冲中。最后将4x分辨率的颜色缓冲降采样成1x的结果进行输出。注意,片元着色器计算的不是采样点颜色,而是片元颜色哦,这是MSAA区别于SSAA的地方,也是它性能提升的地方),那么在前向渲染中,MSAA仅需要对深度缓冲区和颜色缓冲区的容量进行扩增,而在延迟渲染中,MSAA需要扩增的缓冲区远多于这两个缓冲区,造成极大的显存开销,因此unity是不支持在延迟渲染中开启MSAA的,但在理论上这是能做到的。

③    延迟渲染不会在正交投影模式下工作,如果摄像机的投影模式设置为正交模式,那么摄像机会回退到正向渲染模式。这是因为在延迟渲染中,我们有时需要通过深度缓存信息回推位置信息,而透视投影下的深度缓存是非线性的、正交投影下的深度缓存却是线性的,因此适用于透视投影模式的shader代码并不适用于正交投影模式。如果要在延迟渲染中兼容两种投影模式,那么就需要额外的代码判断,这会影响性能,unity引擎的设计者们并不想这么做。

④    延迟渲染对显卡有要求,在不支持延迟渲染的平台上开启延迟渲染,unity会回退到前向渲染模式。默认的几何缓冲区(G-Buffer)渲染目标布局如下(2021.1版本):

Ÿ   RT0,ARGB32:漫反射颜色(RGB),遮挡(A)

Ÿ   RT1,ARGB32:镜面反射颜色(RGB),粗糙度(A)

Ÿ   RT2,ARGB2101010:世界空间法线(RGB),未使用(A)

Ÿ   RT3,ARGB2101010(非HDR)或ARGBHalf(HDR):自发光+环境光+光照贴图+反射探针缓冲区

Ÿ   深度和模板缓冲区

Ÿ   RT4,ARGB32(使用shadowmask或distance shadowmask):光照遮挡值(RGBA)

 

 

2 光照衰减

2.1原理分析

Unity使用查找纹理(lookup table, LUT)来记录空间中各点的光照衰减值,代码如下:

0; half atten_ang = tex2D(_LightTexture0, vertPosLS.xy / vertPosLS.w + 0.5).UNITY_ATTEN_CHANNEL; half atten_dist = tex2D(_LightTextureB0, dot(vertPosLS, vertPosLS).rr).UNITY_ATTEN_CHANNEL; half atten = atten_dir * atten_ang * atten_dist; #else half atten = 1.0; " v:shapes="文本框_x0020_2">

 

①    平行光的衰减值永远为1。

②    点光源的衰减值仅和距离有关,存储这种关系的纹理是_LightTexture0。

③    聚光灯的衰减值不仅和距离有关,还和方向与张角有关。存储距离与衰减关系的纹理是_LightTextureB0;存储张角与衰减关系的纹理是_LightTexture0;方向与衰减的关系不需要存储,只需判断顶点的z坐标是否大于0即可。

④    所有衰减纹理的存储都是基于光源空间的,因此首先要把点的位置从世界空间变换到光源空间下。

⑤    存储距离与衰减关系的纹理如下左图所示,u坐标代表光源空间中的点到光源中心的距离的平方和。因此,对点在光源空间下的坐标进行点积,取结果的首个通道作为uv(代码中的.rr操作)即可作为采样坐标。

⑥    存储张角与衰减关系的纹理如下右图所示,uv坐标代表点与z轴的夹角在xy两轴上的分量相对于最大张角的比值。unity_WorldToLight矩阵会做三件事情:1)将点的坐标从世界空间变换到光源空间;2)对点的坐标进行统一缩放;3)将w分量设为2z/cotHalfSpotAngle。因此vertPosLS.xy/vertPosLS.w = (vertPosLS.xy/z)/tanHalfSpotAngle/2 = tanZAxisAngle/tanHalfSpotAngle/2,tanZAxisAngle/tanHalfSpotAngle代表了点和z轴夹角与最大张角的比值, vertPosLS.xy/vertPosLS.w+0.5对这个比值进行了缩放和偏移,使它的有效范围落在[0,1]内,其中(0.5, 0.5)代表点在光源的z轴上。

⑦    衰减纹理的存储通道在不同机器上可能是不同的,因此需要使用宏UNITY_ATTEN_CHANNEL。                                                                                

   

Fig. 1. 点光源的光照衰减纹理(左)和聚光灯的光照衰减纹理(右)

2.2简单调用

使用宏UNITY_LIGHT_ATTENUATION(destName, input, worldPos)即可。其中,destName是采样得到的光照衰减值和阴影值的乘积,无需额外声明;input是片元着色器的输入结构体,worldPos是顶点位置在世界空间下的坐标,UNITY_SHADOW_ATTENUATION需要用到这两个变量来对阴影映射纹理进行采样。

 

 

 

3 阴影生成

3.1原理分析(ShadowMap

3.1.1传统的阴影映射技术

①    在SubShader或Fallback中寻找LightMode标签为ShadowCaster的Pass。

②    调用该pass生成光源空间下的深度纹理(阴影映射纹理)。

③    在其他pass中将片元的位置变换到光源空间,并使用xy分量对阴影映射纹理采样。

④ 比较光源空间下的深度值与采样得到的深度值,从而判断片元是否在阴影中。(可能产生自遮挡现象,具体见CG部分07_Shadows)

3.1.2屏幕空间的阴影映射技术

①    在SubShader或Fallback中寻找LightMode标签为ShadowCaster的Pass。

②    调用该pass生成摄像机空间下的深度纹理和光源空间下的深度纹理(阴影映射纹理)。

③    调用Hidden/Internal-ScreenSpaceShadows的Pass,根据摄像机深度纹理和阴影映射纹理,在屏幕空间做阴影收集计算(shadows collector),得到屏幕空间的阴影纹理。具体来讲,首先根据摄像机深度纹理还原片元在世界空间下的坐标,而后将片元坐标从世界空间变换到光源空间,再后对阴影映射纹理进行采样,并比较片元在光源空间下的深度值和采样得到的深度值,最后对比较结果处理后写入屏幕空间的阴影纹理中。

④    在其他pass中对屏幕空间的阴影纹理进行采样,和光照结果相乘即可。

 

3.2简单调用

3.2.1投射阴影

①    在片元着色器的输入结构体中声明所需变量V2F_SHADOW_CASTER

②    在顶点着色器中使用宏TRANSFER_SHADOW_CASTER_NORMALOFFSET(fragment input)

③ 在片元着色器中使用宏SHADOW_CASTER_FRAGMENT(fragment input)

3.2.2接收阴影

①    在片元着色器的输入结构体中声明所需变量SHADOW_COORDS(register number)

②    在顶点着色器中使用宏TRANSFER_SHADOW(fragment input)

③    在片元着色器中使用宏SHADOW_ATTENUATION(fragment input)

3.2.3透明度测试的阴影

①    将透明纹理命名为_MainTex

②    将片元裁剪阈值命名为_Cutoff

③    在Fallback中调用Transparent/Cutout/VertexLit,内部的ShadowCaster Pass会用到_MainTex和_Cutoff

④    阴影投射和接收所使用的宏如上两小节

 

3.3常见问题

3.3.1

3.3.2

 

4 内置变量、函数和宏

4.1变量

名称

类型

所在文件

描述

说明

_LightColor0

Float4

UnityLightingCommon.cginc

逐像素光源的颜色。

 

_WorldSpaceLightPos0

Half4/Float4

UnityShaderVariables.cginc

逐像素光源的位置,平行光的w分量为0,否则为1。

 

unity_WorldToLight

Float4x4

AutoLight.cginc

从世界空间到光源空间的变换矩阵。

原名为_LightMatrix0

unity_LightColor

Half4[4]

UnityShaderVariables.cginc

前4个逐顶点光源的颜色。

 

unity_4LightPosX0

unity_4LightPosY0

unity_4LightPosZ0

Float4

UnityShaderVariables.cginc

前4个逐顶点光源在世界空间中的位置。

 

unity_4LightAtten0

Float4

UnityShaderVariables.cginc

前4个逐顶点光源的衰减因子。

 

 

4.2函数

名称

所在文件

描述

原理

float3 UnityWorldSpceLightDir(float4 pos)

UnityCG.cginc

输入世界空间中的顶点位置,返回世界空间中的光源方向。

仅可用于前向渲染中。

对平行光直接返回_WorldSpaceLightPos0.xyz;对其他光源返回光源位置和顶点位置的向量差。

float3 WorldSpaceLightDir(float4 pos)

UnityCG.cginc

输入模型空间中的顶点位置,返回世界空间中的光源方向。

仅可用于前向渲染中。

调用了UnityWorldSpaceLightDir.

float3 ObjectSpaceLightDir(float4 pos)

UnityCG.cginc

输入模型空间中的顶点位置,返回模型空间中的光源方向。

仅可用于前向渲染中。

先把光源位置_WorldSpaceLightPos0转换到模型空间,再根据光源类型是否为平行光而返回对应数据。

float3 Shade4PointLights(…)

UnityCG.cginc

输入四个逐顶点光源的颜色、位置等信息,计算四个光源的漫反射颜色。

仅可用于前向渲染中。

 

 

4.3

名称

所在文件

描述

原理

SHADOW_COORDS(register number)

AutoLight.cginc

声明一个阴影纹理坐标。

 

TRANSFER_SHADOW(fragment input)

AutoLight.cginc

计算阴影纹理坐标。

1)传统阴影映射技术:将v.vertex从模型空间转换到光源空间,因此要保证顶点着色器的输入是v,且顶点坐标名为vertex

2)屏幕空间阴影映射技术:使用内置的ComputerScreenPos函数计算阴影纹理坐标,该函数要用到齐次裁剪空间的顶点坐标,因此要保证该变量名为pos

SHADOW_ATTENUATION(fragment input)

AutoLight.cginc

使用计算好的坐标对阴影纹理进行采样。

 

UNITY_LIGHT_ATTENUATION(destName, fragment input, worldPos)

AutoLight.cginc

计算当前片元的光照衰减值与阴影值的乘积。

Unity根据不同的光源类型定义了不同的UNITY_LIGHT_ATTENUATION宏,使用如2.1部分的代码对光照衰减纹理进行采样,使用UNITY_SHADOW_ATTENUATION(fragment input, worldPos)对阴影映射纹理进行采样。

posted @ 2021-03-31 19:05  rbcl  阅读(266)  评论(0)    收藏  举报