此贴来源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