Bootstrap

2020-09-24

此贴来源copy的小结

记录Unity的标准着色器实现,基于Unity 2017.1版本的代码进行分析。

Standard Shader

文件位于\DefaultResourcesExtra\Standard.shader

 Standard Shader

Standard Shader中主要有三个分支,一个是SM3.0的Forward渲染实现,一个是Deferred渲染实现,一个是针对SM2.0的Forward渲染实现。

在SM3.0下,Unity实现Forward渲染有两个Pass。第一个是Pass是针对主光源的ForwardBase,第二个Pass是针对其他光源的ForwardAdd。实现两个Pass的顶点着色器和片段着色器函数名称也已经给出,包含在"UnityStandardCoreForward.cginc"文件中。

在UnityStandardCoreForward.cginc文件中出现了分支,一个是Simple实现一个是标准的实现。从学习的目的来讲,主要看Unity的标准实现。

根据上述代码,我们在UnityStandardCore.cginc中找到了顶点着色器和片段着色器的具体实现。为了减轻工作量,先研究Forward渲染的代码。顶点着色器为vertForwardBase/vertForwardAdd,片段着色器为fragForwardBase/fragForwardAdd。ForwardAdd实现和ForwardBase实现类似,只有少量区别。所以主要分析ForwardBase,ForwardAdd会在之后简单介绍与ForwardBase的差异。

vertForwardBase函数

作为一个顶点着色器,vertForwardBase的实现很常规,主要是一系列相关的坐标变换工作。在分析vertForwardBase函数之前,需要先分析一下顶点着色器输出到片段着色器的结构体VertexOutputForwardBase。这部分内容不重要,只会简单的说明一下。

复制代码

 1 struct VertexOutputForwardBase
 2 {
 3     UNITY_POSITION(pos);
 4     float4 tex                          : TEXCOORD0;
 5     half3 eyeVec                        : TEXCOORD1;
 6     half4 tangentToWorldAndPackedData[3]    : TEXCOORD2;    // [3x3:tangentToWorld | 1x3:viewDirForParallax or worldPos]
 7     half4 ambientOrLightmapUV           : TEXCOORD5;    // SH or Lightmap UV
 8     UNITY_SHADOW_COORDS(6)
 9     UNITY_FOG_COORDS(7)
10 
11     // next ones would not fit into SM2.0 limits, but they are always for SM3.0+
12     #if UNITY_REQUIRE_FRAG_WORLDPOS && !UNITY_PACK_WORLDPOS_WITH_TANGENT
13         float3 posWorld                 : TEXCOORD8;
14     #endif
15 
16     UNITY_VERTEX_INPUT_INSTANCE_ID
17     UNITY_VERTEX_OUTPUT_STEREO
18 };

复制代码

总体来说做了以下的工作:

1.定义变量:顶点坐标,纹理坐标,视线向量。UNITY_POSITION(pos)宏定义位于HLSLSupport.cginc,不赘述。

2.tangentToWorldAndPackedData[3]:大小为3x4,其中3x3矩阵为切线空间变换到世界空间矩阵(xyz分量),1x3为视差视线向量或世界坐标(w分量)。

3.定义变量:环境或光照贴图的坐标,阴影坐标,雾坐标。

4.针对SM3.0,定义变量:顶点世界坐标。

5.UNITY_VERTEX_INPUT_INSTANCE_ID为顶点实例化一个ID,UNITY_VERTEX_OUTPUT_STEREO来声明该顶点是否位于视线域中,来判断这个顶点是否输出到片段着色器。两个宏定义位于UnityInstancing.cginc中,GPU Instancing所需,这里不赘述。

顶点函数vertForwardBase用于填充VertexOutputForwardBase结构体。

 vertForwardBase

输入为VertexInput结构体,分析见后文。

总体来说做了以下的工作:

1.初始化顶点信息。这部分是GPU Instancing的相关宏定义,位于UnityInstancing.cginc中。

2.顶点世界坐标计算,并根据Shader Mode的不同来将其存储在posWorld(SM3.0)或tangentToWorldAndPackedData[3]的w分量(SM2.0)中。在SM3.0下,tangentToWorldAndPackedData[3]的w分量用来存储视差视线。

3.计算裁剪空间顶点坐标,纹理坐标,世界空间视线以及法线。TexCoords函数实现在UnityStandardInput.cginc,UnityObjectToClipPosInstanced在UnityInstancing.cginc,NormalizePerVertexNormal在UnityStandardCore.cginc,不赘述。

4.计算切线空间变换到世界空间矩阵。CreateTangentToWorldPerVertex位于UnityStandardUtils.cginc。

5.阴影坐标转换,雾坐标转换。UNITY_TRANSFER_SHADOW位于AutoLight.cginc,UNITY_TRANSFER_FOG位于UnityCG.cginc。雾的计算会根据SM不同,选择逐顶点或逐像素的计算。

6.视差视线计算,ObjSpaceViewDir和rotation都位于UnityCG.cginc

7.环境或光照贴图纹理坐标的计算,VertexGIForward的实现位于UnityStandardCore.cginc。

VertexInput结构体位于UnityStandardInput.cginc,具体实现如下。

 VertexInput

VertexInput结构体包含了Unity提供给顶点着色器的模型的各项信息,包括模型空间的顶点坐标,纹理坐标,顶点法线和切线。纹理坐标有三个,第一个是贴图的纹理坐标,第二个是静态光照UV(Bake GI),第三个是动态光照UV(Precompute Realtime GI)。

VertexGIForward主要进行顶点的GI计算,具体的实现分析如下。

 VertexGIForward

输入:VertexInput,顶点世界坐标,世界法线。

Unity的GI实现有两种方式,一种是烘焙GI(Bake GI),一种是预计算实时GI(PRGI,Precompute Realtime GI)。对于烘焙GI来说,lightmap是静态的;对于预计算实时GI来说,lightmap是动态的。预计算实时GI在Unity官方中文论坛有比较详细的介绍,不赘述。

然而,在VertexGIForward的计算中,根据GI的实现方式有三个分支。首先是烘焙GI的实现,unity_LightmapST的xy记录了lightmap的scale值,zw记录了lightmap的offset值。第二个分支是SH(球谐函数)的计算,SH的计算在存在GI的情况下是不进行计算的,因为lightmap中已经包含了漫反射间接环境光照。所以在没有lightmap的情况下,进行SH计算。追求效率,用于SH计算的点光源被设置为了4个,同时unity在QualitySetting中的pixel light count也是设置为4。第三个分支是预计算实时GI的实现,unity_DynamicLightmapST和unity_LightmapST类似。

ambientOrLightmapUV在启用光照贴图的情况下,其xyzw分量用来存储光照贴图的UV。在不启用光照贴图的情况下,其rgb(xyz)分量用来保存SH计算的颜色。

Shade4PointLights,ShadeSHPerVertex函数实现暂时不赘述,Shade4PointLights计算四个点光源的方式比较巧妙,有时间会讲。

fragForwardBaseInternal函数

 fragForwardBaseInternal

总体来说做了以下的工作:

1.初始化片段设置,暂时不赘述。

2.获取设置的主光源信息。UnityLight结构体记录了灯光的方向,颜色。MainLight函数则把主光源的信息填充到结构体中。

UnityLight结构体定义在UnityLightingCommon.cginc中。

 UnityLight

MainLight函数定义在UnityStandardCore.cginc中。

 MainLight

3.计算灯光的衰减信息。

4.计算遮罩,遮罩的意义是利用Occlusion Map以及Occlusion Strength(SM3.0)来控制物体表面接收间接光照的强度。

Occlusion函数——UnityStandardInput.cginc中。

 Occlusion

实现原理比较简单:在SM2.0下,由于指令数限制,直接从遮罩图中采样返回green通道即可;在SM3.0下,采样后,需要多做一步计算。Occlusion Map一般是一张灰度图,Unity这里只使用了它的Green通道。_OcclusionMap和_OcclusionStrength都是Standard Shader暴露给编辑器的变量。

LerpOneTo函数——UnityStandardUtils.cginc

 LerpOneTo

LerpOneTo的实现比较简单,类似Lerp函数的计算,返回值为1-_OcclusionStrength+occ*_OcclusionStrength。其实也就等价于Lerp(1,occ,_OcclusionStrength)。

5.计算片段GI。

在分析Fragment函数之前,先简单过一下Fragment函数用到的几个结构体。

FragmentCommonData:包括了片段函数需要的一些常规的数据,包括漫反射颜色,镜面反射颜色,一减反射率,平滑度,世界空间法线,视线,世界空间坐标,alpha。以及如果是simple模式下,定义反射uvw和切线空间法线。

 FragmentCommonData

UnityGI:记录了一个记录灯光信息的UnityLight对象和一个记录间接光照信息的UnityIndirect对象。

 UnityGI

UnityIndirect结构体的两个变量分别是漫反射颜色和镜面反射颜色。

 UnityIndirect

UnityGIInput——-UnityLightingCommon.cginc

 UnityGIInput

包括一个UnityLight对象,以及片段的世界空间坐标,世界空间视线,灯光的衰减,环境色。光照贴图的UV,出于精度考虑使用float来避免光照贴图采样精度丢失。xy分量是静态光照贴图UV,zw分量是动态光照贴图UV。之后是用于反射探针盒投影,反射探针混合,以及HDR天空的变量,在FragmentGI函数中会用到。

UNITY_SPECCUBE_BOX_PROJECTION&UNITY_SPECCUBE_BLENDING——UnityStandardConfig.cginc。

 TierSettings

TierSettings来控制的,当设置为启用时,会自动生成相关的宏定义。具体使用查看Unity Scripting API。

UNITY_SPECCUBE_BOX_PROJECTION: TierSettings.reflectionProbeBoxProjection——指定反射探针盒投影是否启用。
UNITY_SPECCUBE_BLENDING: TierSettings.reflectionProbeBlending——指定反射探针混合是否启用。

FragmentGI函数——UnityStandardCore.cginc

 FragmentGI

FragmentGI函数主要可以分为两个部分,一个是填充UnityGIInput结构体,一个计算反射调用UnityGlobalIllumination函数的过程。

填充UnityGIInput的过程,灯光+世界空间顶点坐标+观察方向(视线的反方向)+衰减直接赋值即可。随后是光照贴图,在启用了静态光照贴图或者动态光照贴图的情况下,环境光为0,然后获得光照贴图的UV。否则的话,ambient直接使用VertexGIForward计算的rgb值。

然后是反射探针的相关计算,这部分计算需要涉及到一系列变量声明。

Reflection Probes——UnityShaderVariables.cginc

 Reflection Probes

UNITY_DECLARE_TEXCUBE:声明了一个TextureCube类型的对象。
UNITY_DECLARE_TEXCUBE_NOSAMPLER:声明了一个TextureCube类型的对象(无Sampler)。

CBUFFER_START&CBUFFER_END:声明了一块常量缓冲区。

以上的宏定义在HLSLSupport.cginc文件中。

HDR探针将常量缓冲中的对应变量保存。在UNITY_SPECCUBE_BOX_PROJECTION或者UNITY_SPECCUBE_BLENDING被启用的情况下,保存相应的反射探针属性。

如果反射为真的情况下,计算反射,先计算反射的环境数据,包括镜面照明和天空等。然后调用UnityGlobalIllumination函数。

Unity_GlossyEnvironmentData结构体:延迟渲染只有一个cubemap,前向渲染的情况下可以有两个混合的cubemap,不常用应该会被弃用。另外,粗糙度这里是感性粗糙度,因为兼容性而使用了粗糙度的变量名。关于粗糙度和感性粗糙度之间的区别,见后文。

 Unity_GlossyEnvironmentData

UnityGlossyEnvironmentSetup函数:计算感性粗糙度,计算反射,并返回对象。

 UnityGlossyEnvironmentSetup

UnityGlobalIllumination函数

 UnityGlobalIllumination

UnityGlobalIllumination函数位于同名的cginc文件中,同名的函数有四个(形参不同)。有两个函数实现是旧版本的,并在注释上说明了只是为了旧版本兼容即将被移除,所以我们这里只看最新的函数实现。

由FragmentGI函数可知,两种实现分别对应无反射和有反射两种情况。

函数1(无反射):直接返回UnityGI_Base函数的值,UnityGI_Base解释见下文。

函数2(有反射):调用UnityGI_Base函数,得到返回值o_gi 然后调用UnityGI_IndirectSpecular修改o_gi.indirect.specular的值,UnityGI_IndirectSpecular解释见下文。

UnityGI_Base函数

 UnityGI_Base

函数依次实现的是阴影遮罩,SH计算(非静态GI非动态GI),静态GI,动态GI。

ShadowMask阴影遮罩是Unity5.6版本的新特性。

烘焙GI的实现:首先从烘焙的光照贴图中取得对应的像素值,其次根据编码对像素进行译码。如果定义了directional lightmap混合,会进行和烘焙光照贴图类似的过程并进行混合计算。否则的话,直接使用烘焙GI的颜色进行计算。

动态GI的实现:和静态GI的实现类似。

ResetUnityGI函数:将UnityGI结构体初始化,无需多说。

 ResetUnityGI

UnityGI_IndirectSpecular函数

 UnityGI_IndirectSpecular

BoxProjectedCubemapDirection函数

 BoxProjectedCubemapDirection

 6.计算颜色,这部分为重点内容,后文详细讲解。

7.计算自发光。

 Emission

8.应用雾

 UNITY_APPLY_FOG_COLOR

;