记录Unity的标准着色器实现,基于Unity 2017.1版本的代码进行分析。
Standard Shader
文件位于\DefaultResourcesExtra\Standard.shader
1 // Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt) 2 3 Shader "Standard" 4 { 5 Properties 6 { 7 _Color("Color", Color) = (1,1,1,1) 8 _MainTex("Albedo", 2D) = "white" {} 9 10 _Cutoff("Alpha Cutoff", Range(0.0, 1.0)) = 0.5 11 12 _Glossiness("Smoothness", Range(0.0, 1.0)) = 0.5 13 _GlossMapScale("Smoothness Scale", Range(0.0, 1.0)) = 1.0 14 [Enum(Metallic Alpha,0,Albedo Alpha,1)] _SmoothnessTextureChannel ("Smoothness texture channel", Float) = 0 15 16 [Gamma] _Metallic("Metallic", Range(0.0, 1.0)) = 0.0 17 _MetallicGlossMap("Metallic", 2D) = "white" {} 18 19 [ToggleOff] _SpecularHighlights("Specular Highlights", Float) = 1.0 20 [ToggleOff] _GlossyReflections("Glossy Reflections", Float) = 1.0 21 22 _BumpScale("Scale", Float) = 1.0 23 _BumpMap("Normal Map", 2D) = "bump" {} 24 25 _Parallax ("Height Scale", Range (0.005, 0.08)) = 0.02 26 _ParallaxMap ("Height Map", 2D) = "black" {} 27 28 _OcclusionStrength("Strength", Range(0.0, 1.0)) = 1.0 29 _OcclusionMap("Occlusion", 2D) = "white" {} 30 31 _EmissionColor("Color", Color) = (0,0,0) 32 _EmissionMap("Emission", 2D) = "white" {} 33 34 _DetailMask("Detail Mask", 2D) = "white" {} 35 36 _DetailAlbedoMap("Detail Albedo x2", 2D) = "grey" {} 37 _DetailNormalMapScale("Scale", Float) = 1.0 38 _DetailNormalMap("Normal Map", 2D) = "bump" {} 39 40 [Enum(UV0,0,UV1,1)] _UVSec ("UV Set for secondary textures", Float) = 0 41 42 43 // Blending state 44 [HideInInspector] _Mode ("__mode", Float) = 0.0 45 [HideInInspector] _SrcBlend ("__src", Float) = 1.0 46 [HideInInspector] _DstBlend ("__dst", Float) = 0.0 47 [HideInInspector] _ZWrite ("__zw", Float) = 1.0 48 } 49 50 CGINCLUDE 51 #define UNITY_SETUP_BRDF_INPUT MetallicSetup 52 ENDCG 53 54 SubShader 55 { 56 Tags { "RenderType"="Opaque" "PerformanceChecks"="False" } 57 LOD 300 58 59 60 // ------------------------------------------------------------------ 61 // Base forward pass (directional light, emission, lightmaps, ...) 62 Pass 63 { 64 Name "FORWARD" 65 Tags { "LightMode" = "ForwardBase" } 66 67 Blend [_SrcBlend] [_DstBlend] 68 ZWrite [_ZWrite] 69 70 CGPROGRAM 71 #pragma target 3.0 72 73 // ------------------------------------- 74 75 #pragma shader_feature _NORMALMAP 76 #pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON 77 #pragma shader_feature _EMISSION 78 #pragma shader_feature _METALLICGLOSSMAP 79 #pragma shader_feature ___ _DETAIL_MULX2 80 #pragma shader_feature _ _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A 81 #pragma shader_feature _ _SPECULARHIGHLIGHTS_OFF 82 #pragma shader_feature _ _GLOSSYREFLECTIONS_OFF 83 #pragma shader_feature _PARALLAXMAP 84 85 #pragma multi_compile_fwdbase 86 #pragma multi_compile_fog 87 #pragma multi_compile_instancing 88 // Uncomment the following line to enable dithering LOD crossfade. Note: there are more in the file to uncomment for other passes. 89 //#pragma multi_compile _ LOD_FADE_CROSSFADE 90 91 #pragma vertex vertBase 92 #pragma fragment fragBase 93 #include "UnityStandardCoreForward.cginc" 94 95 ENDCG 96 } 97 // ------------------------------------------------------------------ 98 // Additive forward pass (one light per pass) 99 Pass 100 { 101 Name "FORWARD_DELTA" 102 Tags { "LightMode" = "ForwardAdd" } 103 Blend [_SrcBlend] One 104 Fog { Color (0,0,0,0) } // in additive pass fog should be black 105 ZWrite Off 106 ZTest LEqual 107 108 CGPROGRAM 109 #pragma target 3.0 110 111 // ------------------------------------- 112 113 114 #pragma shader_feature _NORMALMAP 115 #pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON 116 #pragma shader_feature _METALLICGLOSSMAP 117 #pragma shader_feature _ _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A 118 #pragma shader_feature _ _SPECULARHIGHLIGHTS_OFF 119 #pragma shader_feature ___ _DETAIL_MULX2 120 #pragma shader_feature _PARALLAXMAP 121 122 #pragma multi_compile_fwdadd_fullshadows 123 #pragma multi_compile_fog 124 // Uncomment the following line to enable dithering LOD crossfade. Note: there are more in the file to uncomment for other passes. 125 //#pragma multi_compile _ LOD_FADE_CROSSFADE 126 127 #pragma vertex vertAdd 128 #pragma fragment fragAdd 129 #include "UnityStandardCoreForward.cginc" 130 131 ENDCG 132 } 133 // ------------------------------------------------------------------ 134 // Shadow rendering pass 135 Pass { 136 Name "ShadowCaster" 137 Tags { "LightMode" = "ShadowCaster" } 138 139 ZWrite On ZTest LEqual 140 141 CGPROGRAM 142 #pragma target 3.0 143 144 // ------------------------------------- 145 146 147 #pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON 148 #pragma shader_feature _METALLICGLOSSMAP 149 #pragma shader_feature _PARALLAXMAP 150 #pragma multi_compile_shadowcaster 151 #pragma multi_compile_instancing 152 // Uncomment the following line to enable dithering LOD crossfade. Note: there are more in the file to uncomment for other passes. 153 //#pragma multi_compile _ LOD_FADE_CROSSFADE 154 155 #pragma vertex vertShadowCaster 156 #pragma fragment fragShadowCaster 157 158 #include "UnityStandardShadow.cginc" 159 160 ENDCG 161 } 162 // ------------------------------------------------------------------ 163 // Deferred pass 164 Pass 165 { 166 Name "DEFERRED" 167 Tags { "LightMode" = "Deferred" } 168 169 CGPROGRAM 170 #pragma target 3.0 171 #pragma exclude_renderers nomrt 172 173 174 // ------------------------------------- 175 176 #pragma shader_feature _NORMALMAP 177 #pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON 178 #pragma shader_feature _EMISSION 179 #pragma shader_feature _METALLICGLOSSMAP 180 #pragma shader_feature _ _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A 181 #pragma shader_feature _ _SPECULARHIGHLIGHTS_OFF 182 #pragma shader_feature ___ _DETAIL_MULX2 183 #pragma shader_feature _PARALLAXMAP 184 185 #pragma multi_compile_prepassfinal 186 #pragma multi_compile_instancing 187 // Uncomment the following line to enable dithering LOD crossfade. Note: there are more in the file to uncomment for other passes. 188 //#pragma multi_compile _ LOD_FADE_CROSSFADE 189 190 #pragma vertex vertDeferred 191 #pragma fragment fragDeferred 192 193 #include "UnityStandardCore.cginc" 194 195 ENDCG 196 } 197 198 // ------------------------------------------------------------------ 199 // Extracts information for lightmapping, GI (emission, albedo, ...) 200 // This pass it not used during regular rendering. 201 Pass 202 { 203 Name "META" 204 Tags { "LightMode"="Meta" } 205 206 Cull Off 207 208 CGPROGRAM 209 #pragma vertex vert_meta 210 #pragma fragment frag_meta 211 212 #pragma shader_feature _EMISSION 213 #pragma shader_feature _METALLICGLOSSMAP 214 #pragma shader_feature _ _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A 215 #pragma shader_feature ___ _DETAIL_MULX2 216 #pragma shader_feature EDITOR_VISUALIZATION 217 218 #include "UnityStandardMeta.cginc" 219 ENDCG 220 } 221 } 222 223 SubShader 224 { 225 Tags { "RenderType"="Opaque" "PerformanceChecks"="False" } 226 LOD 150 227 228 // ------------------------------------------------------------------ 229 // Base forward pass (directional light, emission, lightmaps, ...) 230 Pass 231 { 232 Name "FORWARD" 233 Tags { "LightMode" = "ForwardBase" } 234 235 Blend [_SrcBlend] [_DstBlend] 236 ZWrite [_ZWrite] 237 238 CGPROGRAM 239 #pragma target 2.0 240 241 #pragma shader_feature _NORMALMAP 242 #pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON 243 #pragma shader_feature _EMISSION 244 #pragma shader_feature _METALLICGLOSSMAP 245 #pragma shader_feature _ _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A 246 #pragma shader_feature _ _SPECULARHIGHLIGHTS_OFF 247 #pragma shader_feature _ _GLOSSYREFLECTIONS_OFF 248 // SM2.0: NOT SUPPORTED shader_feature ___ _DETAIL_MULX2 249 // SM2.0: NOT SUPPORTED shader_feature _PARALLAXMAP 250 251 #pragma skip_variants SHADOWS_SOFT DIRLIGHTMAP_COMBINED 252 253 #pragma multi_compile_fwdbase 254 #pragma multi_compile_fog 255 256 #pragma vertex vertBase 257 #pragma fragment fragBase 258 #include "UnityStandardCoreForward.cginc" 259 260 ENDCG 261 } 262 // ------------------------------------------------------------------ 263 // Additive forward pass (one light per pass) 264 Pass 265 { 266 Name "FORWARD_DELTA" 267 Tags { "LightMode" = "ForwardAdd" } 268 Blend [_SrcBlend] One 269 Fog { Color (0,0,0,0) } // in additive pass fog should be black 270 ZWrite Off 271 ZTest LEqual 272 273 CGPROGRAM 274 #pragma target 2.0 275 276 #pragma shader_feature _NORMALMAP 277 #pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON 278 #pragma shader_feature _METALLICGLOSSMAP 279 #pragma shader_feature _ _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A 280 #pragma shader_feature _ _SPECULARHIGHLIGHTS_OFF 281 #pragma shader_feature ___ _DETAIL_MULX2 282 // SM2.0: NOT SUPPORTED shader_feature _PARALLAXMAP 283 #pragma skip_variants SHADOWS_SOFT 284 285 #pragma multi_compile_fwdadd_fullshadows 286 #pragma multi_compile_fog 287 288 #pragma vertex vertAdd 289 #pragma fragment fragAdd 290 #include "UnityStandardCoreForward.cginc" 291 292 ENDCG 293 } 294 // ------------------------------------------------------------------ 295 // Shadow rendering pass 296 Pass { 297 Name "ShadowCaster" 298 Tags { "LightMode" = "ShadowCaster" } 299 300 ZWrite On ZTest LEqual 301 302 CGPROGRAM 303 #pragma target 2.0 304 305 #pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON 306 #pragma shader_feature _METALLICGLOSSMAP 307 #pragma skip_variants SHADOWS_SOFT 308 #pragma multi_compile_shadowcaster 309 310 #pragma vertex vertShadowCaster 311 #pragma fragment fragShadowCaster 312 313 #include "UnityStandardShadow.cginc" 314 315 ENDCG 316 } 317 318 // ------------------------------------------------------------------ 319 // Extracts information for lightmapping, GI (emission, albedo, ...) 320 // This pass it not used during regular rendering. 321 Pass 322 { 323 Name "META" 324 Tags { "LightMode"="Meta" } 325 326 Cull Off 327 328 CGPROGRAM 329 #pragma vertex vert_meta 330 #pragma fragment frag_meta 331 332 #pragma shader_feature _EMISSION 333 #pragma shader_feature _METALLICGLOSSMAP 334 #pragma shader_feature _ _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A 335 #pragma shader_feature ___ _DETAIL_MULX2 336 #pragma shader_feature EDITOR_VISUALIZATION 337 338 #include "UnityStandardMeta.cginc" 339 ENDCG 340 } 341 } 342 343 344 FallBack "VertexLit" 345 CustomEditor "StandardShaderGUI" 346 }
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结构体。
1 VertexOutputForwardBase vertForwardBase (VertexInput v) 2 { 3 UNITY_SETUP_INSTANCE_ID(v); 4 VertexOutputForwardBase o; 5 UNITY_INITIALIZE_OUTPUT(VertexOutputForwardBase, o); 6 UNITY_TRANSFER_INSTANCE_ID(v, o); 7 UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); 8 9 float4 posWorld = mul(unity_ObjectToWorld, v.vertex); 10 #if UNITY_REQUIRE_FRAG_WORLDPOS 11 #if UNITY_PACK_WORLDPOS_WITH_TANGENT 12 o.tangentToWorldAndPackedData[0].w = posWorld.x; 13 o.tangentToWorldAndPackedData[1].w = posWorld.y; 14 o.tangentToWorldAndPackedData[2].w = posWorld.z; 15 #else 16 o.posWorld = posWorld.xyz; 17 #endif 18 #endif 19 o.pos = UnityObjectToClipPos(v.vertex); 20 21 o.tex = TexCoords(v); 22 o.eyeVec = NormalizePerVertexNormal(posWorld.xyz - _WorldSpaceCameraPos); 23 float3 normalWorld = UnityObjectToWorldNormal(v.normal); 24 #ifdef _TANGENT_TO_WORLD 25 float4 tangentWorld = float4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w); 26 27 float3x3 tangentToWorld = CreateTangentToWorldPerVertex(normalWorld, tangentWorld.xyz, tangentWorld.w); 28 o.tangentToWorldAndPackedData[0].xyz = tangentToWorld[0]; 29 o.tangentToWorldAndPackedData[1].xyz = tangentToWorld[1]; 30 o.tangentToWorldAndPackedData[2].xyz = tangentToWorld[2]; 31 #else 32 o.tangentToWorldAndPackedData[0].xyz = 0; 33 o.tangentToWorldAndPackedData[1].xyz = 0; 34 o.tangentToWorldAndPackedData[2].xyz = normalWorld; 35 #endif 36 37 //We need this for shadow receving 38 UNITY_TRANSFER_SHADOW(o, v.uv1); 39 40 o.ambientOrLightmapUV = VertexGIForward(v, posWorld, normalWorld); 41 42 #ifdef _PARALLAXMAP 43 TANGENT_SPACE_ROTATION; 44 half3 viewDirForParallax = mul (rotation, ObjSpaceViewDir(v.vertex)); 45 o.tangentToWorldAndPackedData[0].w = viewDirForParallax.x; 46 o.tangentToWorldAndPackedData[1].w = viewDirForParallax.y; 47 o.tangentToWorldAndPackedData[2].w = viewDirForParallax.z; 48 #endif 49 50 UNITY_TRANSFER_FOG(o,o.pos); 51 return o; 52 }
输入为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,具体实现如下。
1 struct VertexInput 2 { 3 float4 vertex : POSITION; 4 half3 normal : NORMAL; 5 float2 uv0 : TEXCOORD0; 6 float2 uv1 : TEXCOORD1; 7 #if defined(DYNAMICLIGHTMAP_ON) || defined(UNITY_PASS_META) 8 float2 uv2 : TEXCOORD2; 9 #endif 10 #ifdef _TANGENT_TO_WORLD 11 half4 tangent : TANGENT; 12 #endif 13 UNITY_VERTEX_INPUT_INSTANCE_ID 14 };
VertexInput结构体包含了Unity提供给顶点着色器的模型的各项信息,包括模型空间的顶点坐标,纹理坐标,顶点法线和切线。纹理坐标有三个,第一个是贴图的纹理坐标,第二个是静态光照UV(Bake GI),第三个是动态光照UV(Precompute Realtime GI)。
VertexGIForward主要进行顶点的GI计算,具体的实现分析如下。
1 inline half4 VertexGIForward(VertexInput v, float3 posWorld, half3 normalWorld) 2 { 3 half4 ambientOrLightmapUV = 0; 4 // Static lightmaps 5 #ifdef LIGHTMAP_ON 6 ambientOrLightmapUV.xy = v.uv1.xy * unity_LightmapST.xy + unity_LightmapST.zw; 7 ambientOrLightmapUV.zw = 0; 8 // Sample light probe for Dynamic objects only (no static or dynamic lightmaps) 9 #elif UNITY_SHOULD_SAMPLE_SH 10 #ifdef VERTEXLIGHT_ON 11 // Approximated illumination from non-important point lights 12 ambientOrLightmapUV.rgb = Shade4PointLights ( 13 unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, 14 unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb, 15 unity_4LightAtten0, posWorld, normalWorld); 16 #endif 17 18 ambientOrLightmapUV.rgb = ShadeSHPerVertex (normalWorld, ambientOrLightmapUV.rgb); 19 #endif 20 21 #ifdef DYNAMICLIGHTMAP_ON 22 ambientOrLightmapUV.zw = v.uv2.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw; 23 #endif 24 25 return ambientOrLightmapUV; 26 }
输入: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函数
1 half4 fragForwardBaseInternal (VertexOutputForwardBase i) 2 { 3 UNITY_APPLY_DITHER_CROSSFADE(i.pos.xy); 4 5 FRAGMENT_SETUP(s) 6 7 UNITY_SETUP_INSTANCE_ID(i); 8 UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i); 9 10 UnityLight mainLight = MainLight (); 11 UNITY_LIGHT_ATTENUATION(atten, i, s.posWorld); 12 13 half occlusion = Occlusion(i.tex.xy); 14 UnityGI gi = FragmentGI (s, occlusion, i.ambientOrLightmapUV, atten, mainLight); 15 16 half4 c = UNITY_BRDF_PBS (s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect); 17 c.rgb += Emission(i.tex.xy); 18 19 UNITY_APPLY_FOG(i.fogCoord, c.rgb); 20 return OutputForward (c, s.alpha); 21 }
总体来说做了以下的工作:
1.初始化片段设置,暂时不赘述。
2.获取设置的主光源信息。UnityLight结构体记录了灯光的方向,颜色。MainLight函数则把主光源的信息填充到结构体中。
UnityLight结构体定义在UnityLightingCommon.cginc中。
1 struct UnityLight 2 { 3 half3 color; 4 half3 dir; 5 half ndotl; // Deprecated: Ndotl is now calculated on the fly and is no longer stored. Do not used it. 6 };
MainLight函数定义在UnityStandardCore.cginc中。
1 UnityLight MainLight () 2 { 3 UnityLight l; 4 5 l.color = _LightColor0.rgb; 6 l.dir = _WorldSpaceLightPos0.xyz; 7 return l; 8 }
3.计算灯光的衰减信息。
4.计算遮罩,遮罩的意义是利用Occlusion Map以及Occlusion Strength(SM3.0)来控制物体表面接收间接光照的强度。
Occlusion函数——UnityStandardInput.cginc中。
1 half Occlusion(float2 uv) 2 { 3 #if (SHADER_TARGET < 30) 4 // SM20: instruction count limitation 5 // SM20: simpler occlusion 6 return tex2D(_OcclusionMap, uv).g; 7 #else 8 half occ = tex2D(_OcclusionMap, uv).g; 9 return LerpOneTo (occ, _OcclusionStrength); 10 #endif 11 }
实现原理比较简单:在SM2.0下,由于指令数限制,直接从遮罩图中采样返回green通道即可;在SM3.0下,采样后,需要多做一步计算。Occlusion Map一般是一张灰度图,Unity这里只使用了它的Green通道。_OcclusionMap和_OcclusionStrength都是Standard Shader暴露给编辑器的变量。
LerpOneTo函数——UnityStandardUtils.cginc
1 half LerpOneTo(half b, half t) 2 { 3 half oneMinusT = 1 - t; 4 return oneMinusT + b * t; 5 }
LerpOneTo的实现比较简单,类似Lerp函数的计算,返回值为1-_OcclusionStrength+occ*_OcclusionStrength。其实也就等价于Lerp(1,occ,_OcclusionStrength)。
5.计算片段GI。
在分析Fragment函数之前,先简单过一下Fragment函数用到的几个结构体。
FragmentCommonData:包括了片段函数需要的一些常规的数据,包括漫反射颜色,镜面反射颜色,一减反射率,平滑度,世界空间法线,视线,世界空间坐标,alpha。以及如果是simple模式下,定义反射uvw和切线空间法线。
1 struct FragmentCommonData 2 { 3 half3 diffColor, specColor; 4 // Note: smoothness & oneMinusReflectivity for optimization purposes, mostly for DX9 SM2.0 level. 5 // Most of the math is being done on these (1-x) values, and that saves a few precious ALU slots. 6 half oneMinusReflectivity, smoothness; 7 half3 normalWorld, eyeVec, posWorld; 8 half alpha; 9 10 #if UNITY_STANDARD_SIMPLE 11 half3 reflUVW; 12 #endif 13 14 #if UNITY_STANDARD_SIMPLE 15 half3 tangentSpaceNormal; 16 #endif 17 };
UnityGI:记录了一个记录灯光信息的UnityLight对象和一个记录间接光照信息的UnityIndirect对象。
1 struct UnityGI 2 { 3 UnityLight light; 4 UnityIndirect indirect; 5 };
UnityIndirect结构体的两个变量分别是漫反射颜色和镜面反射颜色。
1 struct UnityIndirect 2 { 3 half3 diffuse; 4 half3 specular; 5 };
UnityGIInput——-UnityLightingCommon.cginc
1 struct UnityGIInput 2 { 3 UnityLight light; // pixel light, sent from the engine 4 5 float3 worldPos; 6 half3 worldViewDir; 7 half atten; 8 half3 ambient; 9 10 // interpolated lightmap UVs are passed as full float precision data to fragment shaders 11 // so lightmapUV (which is used as a tmp inside of lightmap fragment shaders) should 12 // also be full float precision to avoid data loss before sampling a texture. 13 float4 lightmapUV; // .xy = static lightmap UV, .zw = dynamic lightmap UV 14 15 #if defined(UNITY_SPECCUBE_BLENDING) || defined(UNITY_SPECCUBE_BOX_PROJECTION) 16 float4 boxMin[2]; 17 #endif 18 #ifdef UNITY_SPECCUBE_BOX_PROJECTION 19 float4 boxMax[2]; 20 float4 probePosition[2]; 21 #endif 22 // HDR cubemap properties, use to decompress HDR texture 23 float4 probeHDR[2]; 24 };
包括一个UnityLight对象,以及片段的世界空间坐标,世界空间视线,灯光的衰减,环境色。光照贴图的UV,出于精度考虑使用float来避免光照贴图采样精度丢失。xy分量是静态光照贴图UV,zw分量是动态光照贴图UV。之后是用于反射探针盒投影,反射探针混合,以及HDR天空的变量,在FragmentGI函数中会用到。
UNITY_SPECCUBE_BOX_PROJECTION&UNITY_SPECCUBE_BLENDING——UnityStandardConfig.cginc。
1 // "platform caps" defines: they are controlled from TierSettings (Editor will determine values and pass them to compiler) 2 // UNITY_SPECCUBE_BOX_PROJECTION: TierSettings.reflectionProbeBoxProjection 3 // UNITY_SPECCUBE_BLENDING: TierSettings.reflectionProbeBlending 4 // UNITY_ENABLE_DETAIL_NORMALMAP: TierSettings.detailNormalMap 5 // UNITY_USE_DITHER_MASK_FOR_ALPHABLENDED_SHADOWS: TierSettings.semitransparentShadows
TierSettings来控制的,当设置为启用时,会自动生成相关的宏定义。具体使用查看Unity Scripting API。
UNITY_SPECCUBE_BOX_PROJECTION: TierSettings.reflectionProbeBoxProjection——指定反射探针盒投影是否启用。
UNITY_SPECCUBE_BLENDING: TierSettings.reflectionProbeBlending——指定反射探针混合是否启用。
FragmentGI函数——UnityStandardCore.cginc
1 inline UnityGI FragmentGI (FragmentCommonData s, half occlusion, half4 i_ambientOrLightmapUV, half atten, UnityLight light, bool reflections) 2 { 3 UnityGIInput d; 4 d.light = light; 5 d.worldPos = s.posWorld; 6 d.worldViewDir = -s.eyeVec; 7 d.atten = atten; 8 #if defined(LIGHTMAP_ON) || defined(DYNAMICLIGHTMAP_ON) 9 d.ambient = 0; 10 d.lightmapUV = i_ambientOrLightmapUV; 11 #else 12 d.ambient = i_ambientOrLightmapUV.rgb; 13 d.lightmapUV = 0; 14 #endif 15 16 d.probeHDR[0] = unity_SpecCube0_HDR; 17 d.probeHDR[1] = unity_SpecCube1_HDR; 18 #if defined(UNITY_SPECCUBE_BLENDING) || defined(UNITY_SPECCUBE_BOX_PROJECTION) 19 d.boxMin[0] = unity_SpecCube0_BoxMin; // .w holds lerp value for blending 20 #endif 21 #ifdef UNITY_SPECCUBE_BOX_PROJECTION 22 d.boxMax[0] = unity_SpecCube0_BoxMax; 23 d.probePosition[0] = unity_SpecCube0_ProbePosition; 24 d.boxMax[1] = unity_SpecCube1_BoxMax; 25 d.boxMin[1] = unity_SpecCube1_BoxMin; 26 d.probePosition[1] = unity_SpecCube1_ProbePosition; 27 #endif 28 29 if(reflections) 30 { 31 Unity_GlossyEnvironmentData g = UnityGlossyEnvironmentSetup(s.smoothness, -s.eyeVec, s.normalWorld, s.specColor); 32 // Replace the reflUVW if it has been compute in Vertex shader. Note: the compiler will optimize the calcul in UnityGlossyEnvironmentSetup itself 33 #if UNITY_STANDARD_SIMPLE 34 g.reflUVW = s.reflUVW; 35 #endif 36 37 return UnityGlobalIllumination (d, occlusion, s.normalWorld, g); 38 } 39 else 40 { 41 return UnityGlobalIllumination (d, occlusion, s.normalWorld); 42 } 43 }
FragmentGI函数主要可以分为两个部分,一个是填充UnityGIInput结构体,一个计算反射调用UnityGlobalIllumination函数的过程。
填充UnityGIInput的过程,灯光+世界空间顶点坐标+观察方向(视线的反方向)+衰减直接赋值即可。随后是光照贴图,在启用了静态光照贴图或者动态光照贴图的情况下,环境光为0,然后获得光照贴图的UV。否则的话,ambient直接使用VertexGIForward计算的rgb值。
然后是反射探针的相关计算,这部分计算需要涉及到一系列变量声明。
Reflection Probes——UnityShaderVariables.cginc
1 UNITY_DECLARE_TEXCUBE(unity_SpecCube0); 2 UNITY_DECLARE_TEXCUBE_NOSAMPLER(unity_SpecCube1); 3 4 CBUFFER_START(UnityReflectionProbes) 5 float4 unity_SpecCube0_BoxMin; 6 float4 unity_SpecCube0_ProbePosition; 7 half4 unity_SpecCube0_HDR; 8 9 float4 unity_SpecCube1_BoxMax; 10 float4 unity_SpecCube1_BoxMin; 11 float4 unity_SpecCube1_ProbePosition; 12 half4 unity_SpecCube1_HDR; 13 CBUFFER_END
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,不常用应该会被弃用。另外,粗糙度这里是感性粗糙度,因为兼容性而使用了粗糙度的变量名。关于粗糙度和感性粗糙度之间的区别,见后文。
1 struct Unity_GlossyEnvironmentData 2 { 3 // - Deferred case have one cubemap 4 // - Forward case can have two blended cubemap (unusual should be deprecated). 5 6 // Surface properties use for cubemap integration 7 half roughness; // CAUTION: This is perceptualRoughness but because of compatibility this name can't be change :( 8 half3 reflUVW; 9 };
UnityGlossyEnvironmentSetup函数:计算感性粗糙度,计算反射,并返回对象。
1 Unity_GlossyEnvironmentData UnityGlossyEnvironmentSetup(half Smoothness, half3 worldViewDir, half3 Normal, half3 fresnel0) 2 { 3 Unity_GlossyEnvironmentData g; 4 5 g.roughness /* perceptualRoughness */ = SmoothnessToPerceptualRoughness(Smoothness); 6 g.reflUVW = reflect(-worldViewDir, Normal); 7 8 return g; 9 }
UnityGlobalIllumination函数
1 inline UnityGI UnityGlobalIllumination (UnityGIInput data, half occlusion, half3 normalWorld) 2 { 3 return UnityGI_Base(data, occlusion, normalWorld); 4 } 5 6 inline UnityGI UnityGlobalIllumination (UnityGIInput data, half occlusion, half3 normalWorld, Unity_GlossyEnvironmentData glossIn) 7 { 8 UnityGI o_gi = UnityGI_Base(data, occlusion, normalWorld); 9 o_gi.indirect.specular = UnityGI_IndirectSpecular(data, occlusion, glossIn); 10 return o_gi; 11 }
UnityGlobalIllumination函数位于同名的cginc文件中,同名的函数有四个(形参不同)。有两个函数实现是旧版本的,并在注释上说明了只是为了旧版本兼容即将被移除,所以我们这里只看最新的函数实现。
由FragmentGI函数可知,两种实现分别对应无反射和有反射两种情况。
函数1(无反射):直接返回UnityGI_Base函数的值,UnityGI_Base解释见下文。
函数2(有反射):调用UnityGI_Base函数,得到返回值o_gi 然后调用UnityGI_IndirectSpecular修改o_gi.indirect.specular的值,UnityGI_IndirectSpecular解释见下文。
UnityGI_Base函数
1 inline UnityGI UnityGI_Base(UnityGIInput data, half occlusion, half3 normalWorld) 2 { 3 UnityGI o_gi; 4 ResetUnityGI(o_gi); 5 6 // Base pass with Lightmap support is responsible for handling ShadowMask / blending here for performance reason 7 #if defined(HANDLE_SHADOWS_BLENDING_IN_GI) 8 half bakedAtten = UnitySampleBakedOcclusion(data.lightmapUV.xy, data.worldPos); 9 float zDist = dot(_WorldSpaceCameraPos - data.worldPos, UNITY_MATRIX_V[2].xyz); 10 float fadeDist = UnityComputeShadowFadeDistance(data.worldPos, zDist); 11 data.atten = UnityMixRealtimeAndBakedShadows(data.atten, bakedAtten, UnityComputeShadowFade(fadeDist)); 12 #endif 13 14 o_gi.light = data.light; 15 o_gi.light.color *= data.atten; 16 17 #if UNITY_SHOULD_SAMPLE_SH 18 o_gi.indirect.diffuse = ShadeSHPerPixel (normalWorld, data.ambient, data.worldPos); 19 #endif 20 21 #if defined(LIGHTMAP_ON) 22 // Baked lightmaps 23 half4 bakedColorTex = UNITY_SAMPLE_TEX2D(unity_Lightmap, data.lightmapUV.xy); 24 half3 bakedColor = DecodeLightmap(bakedColorTex); 25 26 #ifdef DIRLIGHTMAP_COMBINED 27 fixed4 bakedDirTex = UNITY_SAMPLE_TEX2D_SAMPLER (unity_LightmapInd, unity_Lightmap, data.lightmapUV.xy); 28 o_gi.indirect.diffuse = DecodeDirectionalLightmap (bakedColor, bakedDirTex, normalWorld); 29 30 #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN) 31 ResetUnityLight(o_gi.light); 32 o_gi.indirect.diffuse = SubtractMainLightWithRealtimeAttenuationFromLightmap (o_gi.indirect.diffuse, data.atten, bakedColorTex, normalWorld); 33 #endif 34 35 #else // not directional lightmap 36 o_gi.indirect.diffuse = bakedColor; 37 38 #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN) 39 ResetUnityLight(o_gi.light); 40 o_gi.indirect.diffuse = SubtractMainLightWithRealtimeAttenuationFromLightmap(o_gi.indirect.diffuse, data.atten, bakedColorTex, normalWorld); 41 #endif 42 43 #endif 44 #endif 45 46 #ifdef DYNAMICLIGHTMAP_ON 47 // Dynamic lightmaps 48 fixed4 realtimeColorTex = UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, data.lightmapUV.zw); 49 half3 realtimeColor = DecodeRealtimeLightmap (realtimeColorTex); 50 51 #ifdef DIRLIGHTMAP_COMBINED 52 half4 realtimeDirTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_DynamicDirectionality, unity_DynamicLightmap, data.lightmapUV.zw); 53 o_gi.indirect.diffuse += DecodeDirectionalLightmap (realtimeColor, realtimeDirTex, normalWorld); 54 #else 55 o_gi.indirect.diffuse += realtimeColor; 56 #endif 57 #endif 58 59 o_gi.indirect.diffuse *= occlusion; 60 return o_gi; 61 }
函数依次实现的是阴影遮罩,SH计算(非静态GI非动态GI),静态GI,动态GI。
ShadowMask阴影遮罩是Unity5.6版本的新特性。
烘焙GI的实现:首先从烘焙的光照贴图中取得对应的像素值,其次根据编码对像素进行译码。如果定义了directional lightmap混合,会进行和烘焙光照贴图类似的过程并进行混合计算。否则的话,直接使用烘焙GI的颜色进行计算。
动态GI的实现:和静态GI的实现类似。
ResetUnityGI函数:将UnityGI结构体初始化,无需多说。
1 inline void ResetUnityGI(out UnityGI outGI) 2 { 3 ResetUnityLight(outGI.light); 4 outGI.indirect.diffuse = 0; 5 outGI.indirect.specular = 0; 6 }
UnityGI_IndirectSpecular函数
1 inline half3 UnityGI_IndirectSpecular(UnityGIInput data, half occlusion, Unity_GlossyEnvironmentData glossIn) 2 { 3 half3 specular; 4 5 #ifdef UNITY_SPECCUBE_BOX_PROJECTION 6 // we will tweak reflUVW in glossIn directly (as we pass it to Unity_GlossyEnvironment twice for probe0 and probe1), so keep original to pass into BoxProjectedCubemapDirection 7 half3 originalReflUVW = glossIn.reflUVW; 8 glossIn.reflUVW = BoxProjectedCubemapDirection (originalReflUVW, data.worldPos, data.probePosition[0], data.boxMin[0], data.boxMax[0]); 9 #endif 10 11 #ifdef _GLOSSYREFLECTIONS_OFF 12 specular = unity_IndirectSpecColor.rgb; 13 #else 14 half3 env0 = Unity_GlossyEnvironment (UNITY_PASS_TEXCUBE(unity_SpecCube0), data.probeHDR[0], glossIn); 15 #ifdef UNITY_SPECCUBE_BLENDING 16 const float kBlendFactor = 0.99999; 17 float blendLerp = data.boxMin[0].w; 18 UNITY_BRANCH 19 if (blendLerp < kBlendFactor) 20 { 21 #ifdef UNITY_SPECCUBE_BOX_PROJECTION 22 glossIn.reflUVW = BoxProjectedCubemapDirection (originalReflUVW, data.worldPos, data.probePosition[1], data.boxMin[1], data.boxMax[1]); 23 #endif 24 25 half3 env1 = Unity_GlossyEnvironment (UNITY_PASS_TEXCUBE_SAMPLER(unity_SpecCube1,unity_SpecCube0), data.probeHDR[1], glossIn); 26 specular = lerp(env1, env0, blendLerp); 27 } 28 else 29 { 30 specular = env0; 31 } 32 #else 33 specular = env0; 34 #endif 35 #endif 36 37 return specular * occlusion; 38 } 39 40 // Deprecated old prototype but can't be move to Deprecated.cginc file due to order dependency 41 inline half3 UnityGI_IndirectSpecular(UnityGIInput data, half occlusion, half3 normalWorld, Unity_GlossyEnvironmentData glossIn) 42 { 43 // normalWorld is not used 44 return UnityGI_IndirectSpecular(data, occlusion, glossIn); 45 }
BoxProjectedCubemapDirection函数
1 inline half3 BoxProjectedCubemapDirection (half3 worldRefl, float3 worldPos, float4 cubemapCenter, float4 boxMin, float4 boxMax) 2 { 3 // Do we have a valid reflection probe? 4 UNITY_BRANCH 5 if (cubemapCenter.w > 0.0) 6 { 7 half3 nrdir = normalize(worldRefl); 8 9 #if 1 10 half3 rbmax = (boxMax.xyz - worldPos) / nrdir; 11 half3 rbmin = (boxMin.xyz - worldPos) / nrdir; 12 13 half3 rbminmax = (nrdir > 0.0f) ? rbmax : rbmin; 14 15 #else // Optimized version 16 half3 rbmax = (boxMax.xyz - worldPos); 17 half3 rbmin = (boxMin.xyz - worldPos); 18 19 half3 select = step (half3(0,0,0), nrdir); 20 half3 rbminmax = lerp (rbmax, rbmin, select); 21 rbminmax /= nrdir; 22 #endif 23 24 half fa = min(min(rbminmax.x, rbminmax.y), rbminmax.z); 25 26 worldPos -= cubemapCenter.xyz; 27 worldRefl = worldPos + nrdir * fa; 28 } 29 return worldRefl; 30 }
6.计算颜色,这部分为重点内容,后文详细讲解。
7.计算自发光。
1 half3 Emission(float2 uv) 2 { 3 #ifndef _EMISSION 4 return 0; 5 #else 6 return tex2D(_EmissionMap, uv).rgb * _EmissionColor.rgb; 7 #endif 8 }
8.应用雾
1 // ------------------------------------------------------------------ 2 // Fog helpers 3 // 4 // multi_compile_fog Will compile fog variants. 5 // UNITY_FOG_COORDS(texcoordindex) Declares the fog data interpolator. 6 // UNITY_TRANSFER_FOG(outputStruct,clipspacePos) Outputs fog data from the vertex shader. 7 // UNITY_APPLY_FOG(fogData,col) Applies fog to color "col". Automatically applies black fog when in forward-additive pass. 8 // Can also use UNITY_APPLY_FOG_COLOR to supply your own fog color. 9 10 // In case someone by accident tries to compile fog code in one of the g-buffer or shadow passes: 11 // treat it as fog is off. 12 #if defined(UNITY_PASS_PREPASSBASE) || defined(UNITY_PASS_DEFERRED) || defined(UNITY_PASS_SHADOWCASTER) 13 #undef FOG_LINEAR 14 #undef FOG_EXP 15 #undef FOG_EXP2 16 #endif 17 18 #if defined(UNITY_REVERSED_Z) 19 //D3d with reversed Z => z clip range is [near, 0] -> remapping to [0, far] 20 //max is required to protect ourselves from near plane not being correct/meaningfull in case of oblique matrices. 21 #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) max(((1.0-(coord)/_ProjectionParams.y)*_ProjectionParams.z),0) 22 #elif UNITY_UV_STARTS_AT_TOP 23 //D3d without reversed z => z clip range is [0, far] -> nothing to do 24 #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) (coord) 25 #else 26 //Opengl => z clip range is [-near, far] -> should remap in theory but dont do it in practice to save some perf (range is close enought) 27 #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) (coord) 28 #endif 29 30 #if defined(FOG_LINEAR) 31 // factor = (end-z)/(end-start) = z * (-1/(end-start)) + (end/(end-start)) 32 #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = (coord) * unity_FogParams.z + unity_FogParams.w 33 #elif defined(FOG_EXP) 34 // factor = exp(-density*z) 35 #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = unity_FogParams.y * (coord); unityFogFactor = exp2(-unityFogFactor) 36 #elif defined(FOG_EXP2) 37 // factor = exp(-(density*z)^2) 38 #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = unity_FogParams.x * (coord); unityFogFactor = exp2(-unityFogFactor*unityFogFactor) 39 #else 40 #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = 0.0 41 #endif 42 43 #define UNITY_CALC_FOG_FACTOR(coord) UNITY_CALC_FOG_FACTOR_RAW(UNITY_Z_0_FAR_FROM_CLIPSPACE(coord)) 44 45 #define UNITY_FOG_COORDS_PACKED(idx, vectype) vectype fogCoord : TEXCOORD##idx; 46 47 #if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2) 48 #define UNITY_FOG_COORDS(idx) UNITY_FOG_COORDS_PACKED(idx, float1) 49 50 #if (SHADER_TARGET < 30) || defined(SHADER_API_MOBILE) 51 // mobile or SM2.0: calculate fog factor per-vertex 52 #define UNITY_TRANSFER_FOG(o,outpos) UNITY_CALC_FOG_FACTOR((outpos).z); o.fogCoord.x = unityFogFactor 53 #else 54 // SM3.0 and PC/console: calculate fog distance per-vertex, and fog factor per-pixel 55 #define UNITY_TRANSFER_FOG(o,outpos) o.fogCoord.x = (outpos).z 56 #endif 57 #else 58 #define UNITY_FOG_COORDS(idx) 59 #define UNITY_TRANSFER_FOG(o,outpos) 60 #endif 61 62 #define UNITY_FOG_LERP_COLOR(col,fogCol,fogFac) col.rgb = lerp((fogCol).rgb, (col).rgb, saturate(fogFac)) 63 64 65 #if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2) 66 #if (SHADER_TARGET < 30) || defined(SHADER_API_MOBILE) 67 // mobile or SM2.0: fog factor was already calculated per-vertex, so just lerp the color 68 #define UNITY_APPLY_FOG_COLOR(coord,col,fogCol) UNITY_FOG_LERP_COLOR(col,fogCol,(coord).x) 69 #else 70 // SM3.0 and PC/console: calculate fog factor and lerp fog color 71 #define UNITY_APPLY_FOG_COLOR(coord,col,fogCol) UNITY_CALC_FOG_FACTOR((coord).x); UNITY_FOG_LERP_COLOR(col,fogCol,unityFogFactor) 72 #endif 73 #else 74 #define UNITY_APPLY_FOG_COLOR(coord,col,fogCol) 75 #endif 76 77 #ifdef UNITY_PASS_FORWARDADD 78 #define UNITY_APPLY_FOG(coord,col) UNITY_APPLY_FOG_COLOR(coord,col,fixed4(0,0,0,0)) 79 #else 80 #define UNITY_APPLY_FOG(coord,col) UNITY_APPLY_FOG_COLOR(coord,col,unity_FogColor) 81 #endif
9.返回颜色和Alpha值。
UNITY_BRDF_PBS函数
代码中有三个BRDF的实现函数,对应不同的BRDF模型实现。这里主要分析第一个BRDF函数,也就是BRDF1_Unity_PBS,使用的BRDF模型的公式可以参考:http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html
首先,我们要清楚BRDF1_Unity_PBS函数的依据,即根据Torrance-Sparrow 微表面模型的公式:
f(l,v)=D(h)F(v,h)G(l,v,h)/(4(n⋅l)(n⋅v))
以及BRDF公式:
BRDF = kD / pi + kS * (D * V * F) / 4
然后拆分公式,一项项的实现。
D——微表面分布项
V——遮挡可见性项
F——菲涅尔反射项
kD——漫反射系数
kS——镜面反射系数
Note:V(Visibility)项即G(l,v,h)/(4(n⋅l)(n⋅v))的集合。
菲涅尔反射F
Schlick菲涅尔反射的公式为:
F=F0+(1-F0)*(1-(H*V))^5
F0:光线垂直入射时的表面反射率
H:半角向量
V:视线
此处输入的参数cosA,为saturate(dot(H,V))的值,也可能是abs(dot(H,V))。
1 inline half3 FresnelTerm (half3 F0, half cosA) 2 { 3 half t = Pow5 (1 - cosA); // ala Schlick interpoliation 4 return F0 + (1-F0) * t; 5 }
V&D分支
V项和D项,在代码中出现了分支,用来选择不同的模型:
如果UNITY_BRDF_GGX为真,V项和D项使用GGX的公式来实现。
否则,V项和D项使用Smith-Beckmann和Blinn-Phong公式实现。
GGX相比Blinn-Phong,它更接近粗糙表面上真实反射的外观。
分支选择代码如下:
1 #if UNITY_BRDF_GGX 2 // GGX with roughtness to 0 would mean no specular at all, using max(roughness, 0.002) here to match HDrenderloop roughtness remapping. 3 roughness = max(roughness, 0.002); 4 half V = SmithJointGGXVisibilityTerm (nl, nv, roughness); 5 float D = GGXTerm (nh, roughness); 6 #else 7 // Legacy 8 half V = SmithBeckmannVisibilityTerm (nl, nv, roughness); 9 half D = NDFBlinnPhongNormalizedTerm (nh, PerceptualRoughnessToSpecPower(perceptualRoughness)); 10 #endif
遮挡可见性项V
依次分析两个Visbility函数。首先是SmithJointGGXVisibilityTerm ,Unity使用了简化版的近似公式。
在Unity中V项即是公式中的G项,只是Unity为了简化运算,使得V=G/(4(n⋅l)(n⋅v))。
SmithJointGGXVisibilityTerm使用了Smith-Joint GGX公式,具体的G项公式见:
https://hal.inria.fr/hal-00942452v1/document
Page 26 ——section 5 Equation (21)
计算lambda的公式见:
Page 13——section 3.2
χ+(α):Heaviside function: 1 if a > 0 and 0 if a ≤ 0
上面就是Original formulation这部分被注释掉的原始公式了,正式的代码对上述公式进行了以下优化:
1.每个lambda都+0.5,来抵消 G = 1 / (1 + lambda_v + lambda_l)中分母的1。
2.每项lambda乘(2.0f * NdotL * NdotV)进行化简并整理即可得出。
3.V=G/(4(n⋅l)(n⋅v))的计算,来抵消部分上式乘的(2.0f * NdotL * NdotV)。
当然,因为#if 0以上的代码都没有执行……
Unity最后使用了Smith-Joint的近似公式(UE4使用了同样的公式),因为上述的代码依旧很复杂。考虑到整体的性能,牺牲一部分难以察觉到的精度来提升效率是必要的。
lambdaV = NdotL * sqrt((-NdotV * a2 + NdotV) * NdotV + a2);
lambdaV = NdotL * sqrt((a2-1)* NdotV2 + a2);
lambdaV ≈ NdotL * ((a-1)* NdotV + a);
1e-5f用来避免分母为0的情况。
1 inline half SmithJointGGXVisibilityTerm (half NdotL, half NdotV, half roughness) 2 { 3 #if 0 4 // Original formulation: 5 // lambda_v = (-1 + sqrt(a2 * (1 - NdotL2) / NdotL2 + 1)) * 0.5f; 6 // lambda_l = (-1 + sqrt(a2 * (1 - NdotV2) / NdotV2 + 1)) * 0.5f; 7 // G = 1 / (1 + lambda_v + lambda_l); 8 9 // Reorder code to be more optimal 10 half a = roughness; 11 half a2 = a * a; 12 13 half lambdaV = NdotL * sqrt((-NdotV * a2 + NdotV) * NdotV + a2); 14 half lambdaL = NdotV * sqrt((-NdotL * a2 + NdotL) * NdotL + a2); 15 16 // Simplify visibility term: (2.0f * NdotL * NdotV) / ((4.0f * NdotL * NdotV) * (lambda_v + lambda_l + 1e-5f)); 17 return 0.5f / (lambdaV + lambdaL + 1e-5f); // This function is not intended to be running on Mobile, 18 // therefore epsilon is smaller than can be represented by half 19 #else 20 // Approximation of the above formulation (simplify the sqrt, not mathematically correct but close enough) 21 half a = roughness; 22 half lambdaV = NdotL * (NdotV * (1 - a) + a); 23 half lambdaL = NdotV * (NdotL * (1 - a) + a); 24 25 return 0.5f / (lambdaV + lambdaL + 1e-5f); 26 #endif 27 }
然后是下一个V项的实现,SmithBeckmannVisibilityTerm函数,并在函数中调用了SmithVisibilityTerm函数。
使用的是SmithBeckmann公式,公式具体见首段链接。
SmithBeckmannVisibilityTerm函数计算了k的值,并调用SmithVisibilityTerm函数进一步计算,*0.25是先抵消V=G/(4(n⋅l)(n⋅v))中的4。
SmithBeckmann公式的分子在SmithVisibilityTerm中的gL&gV计算中与V=G/(4(n⋅l)(n⋅v))中的(n⋅l)(n⋅v)消项。
1 inline half SmithBeckmannVisibilityTerm (half NdotL, half NdotV, half roughness) 2 { 3 half c = 0.797884560802865h; // c = sqrt(2 / Pi) 4 half k = roughness * c; 5 return SmithVisibilityTerm (NdotL, NdotV, k) * 0.25f; // * 0.25 is the 1/4 of the visibility term 6 }
1 inline half SmithVisibilityTerm (half NdotL, half NdotV, half k) 2 { 3 half gL = NdotL * (1-k) + k; 4 half gV = NdotV * (1-k) + k; 5 return 1.0 / (gL * gV + 1e-5f); // This function is not intended to be running on Mobile, 6 // therefore epsilon is smaller than can be represented by half 7 }
微表面分布项D
微表面分布项依旧有两个实现,依旧从GGX的实现开始。
没什么好说的,这里直接根据首段链接里的公式转换为代码即可。
UNITY_INV_PI =1 / UNITY_PI
1 inline float GGXTerm (float NdotH, float roughness) 2 { 3 float a2 = roughness * roughness; 4 float d = (NdotH * a2 - NdotH) * NdotH + 1.0f; // 2 mad 5 return UNITY_INV_PI * a2 / (d * d + 1e-7f); // This function is not intended to be running on Mobile, 6 // therefore epsilon is smaller than what can be represented by half 7 }
然后是BlinnPhong的D项实现,NDFBlinnPhongNormalizedTerm函数。这里需要注意的是函数的输入参数n,是公式中的Power项的计算,实现函数为PerceptualRoughnessToSpecPower。
normTerm即公式中的正态分布项(首项)。
1 inline half NDFBlinnPhongNormalizedTerm (half NdotH, half n) 2 { 3 // norm = (n+2)/(2*pi) 4 half normTerm = (n + 2.0) * (0.5/UNITY_PI); 5 6 half specTerm = pow (NdotH, n); 7 return specTerm * normTerm; 8 }
PerceptualRoughnessToSpecPower函数的具体实现如下,PerceptualRoughnessToRoughness函数是返回perceptualRoughness的平方(UE4的roughness=Unity的perceptualRoughness)。
1 inline half PerceptualRoughnessToSpecPower (half perceptualRoughness) 2 { 3 half m = PerceptualRoughnessToRoughness(perceptualRoughness); // m is the true academic roughness. 4 half sq = max(1e-4f, m*m); 5 half n = (2.0 / sq) - 2.0; // https://dl.dropboxusercontent.com/u/55891920/papers/mm_brdf.pdf 6 n = max(n, 1e-4f); // prevent possible cases of pow(0,0), which could happen when roughness is 1.0 and NdotH is zero 7 return n; 8 }
PerceptualRoughnessToRoughness函数的具体实现如下,用于将感性粗糙度计算为学术意义上的粗糙度。perceptualRoughness的值等于1-smoothness,在SmoothnessToPerceptualRoughness函数中实现。
1 float PerceptualRoughnessToRoughness(float perceptualRoughness) 2 { 3 return perceptualRoughness * perceptualRoughness; 4 }
SmoothnessToPerceptualRoughness函数的具体实现,用于计算感性粗糙度,smoothness即材质的光滑度贴图/参数。
1 float SmoothnessToPerceptualRoughness(float smoothness) 2 { 3 return (1 - smoothness); 4 }
以上,Unity BRDF公式中的D/V/F项全部实现。
BRDF1_Unity_PBS函数
接下来,开始对代码中的BRDF1_Unity_PBS函数进行分析。由于函数代码比较长,在分析时,会对函数进行分块解释。同时,还会对调用的函数进行展示和讲解,所以为方便观看,我会把每块代码都标序号。
Part-1
输入:漫反射颜色,镜面反射颜色,一减反射率,平滑度,法线,视线,灯光,GI
1.计算感性粗糙度,实现函数见上文。
2.计算半角向量。
3.这段注释涉及到NdotV的取值问题。对于可见的像素来说,NdotV的取值不能为负值,但是因为透视投影和法线映射会造成这种情况。在这种情况下,法线应该修改的有效(例如面向摄像机)而不会造成奇怪的异常。但是这个修改的操作会占用一些ALU,是用户所不希望的。另一种方法是取NdotV的绝对值(不太正确,但还可以)。下面定义的宏用来控制两种实现方式,如果你的平台算术逻辑单元紧张的话,那就会将负值设置为0。这种校正对于使用Smith-Joint GGX能见度函数是很有用的,因为会导致粗糙表面的高光边缘异常会更明显。
Note:默认情况下禁用这块代码,因为跟SpeedTree使用的双面光照不兼容。
定义宏UNITY_HANDLE_CORRECTLY_NEGATIVE_NDOTV ,值为0。
0:取NdotV的绝对值。
1:计算NdotV,然后判断值的正负。若值为正,返回normal;若值为负,返回normal + viewDir * (-shiftAmount + 1e-5f)。最后,根据返回的值计算NdotV。
normal + viewDir * (-shiftAmount + 1e-5f)其实是对normal向量向视线向量接近的计算。对返回的结果应该进行重新规范化,为了节省ALU并没有这么做。之后的NdotV计算,saturate()其实没有必要了。但是因为对于操作的输出应用saturate()是没有开销的,因此这里依然使用了saturate()。
1 half4 BRDF1_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness, 2 float3 normal, float3 viewDir, 3 UnityLight light, UnityIndirect gi) 4 { 5 float perceptualRoughness = SmoothnessToPerceptualRoughness (smoothness); 6 float3 halfDir = Unity_SafeNormalize (float3(light.dir) + viewDir); 7 8 // NdotV should not be negative for visible pixels, but it can happen due to perspective projection and normal mapping 9 // In this case normal should be modified to become valid (i.e facing camera) and not cause weird artifacts. 10 // but this operation adds few ALU and users may not want it. Alternative is to simply take the abs of NdotV (less correct but works too). 11 // Following define allow to control this. Set it to 0 if ALU is critical on your platform. 12 // This correction is interesting for GGX with SmithJoint visibility function because artifacts are more visible in this case due to highlight edge of rough surface 13 // Edit: Disable this code by default for now as it is not compatible with two sided lighting used in SpeedTree. 14 #define UNITY_HANDLE_CORRECTLY_NEGATIVE_NDOTV 0 15 16 #if UNITY_HANDLE_CORRECTLY_NEGATIVE_NDOTV 17 // The amount we shift the normal toward the view vector is defined by the dot product. 18 half shiftAmount = dot(normal, viewDir); 19 normal = shiftAmount < 0.0f ? normal + viewDir * (-shiftAmount + 1e-5f) : normal; 20 // A re-normalization should be applied here but as the shift is small we don't do it to save ALU. 21 //normal = normalize(normal); 22 23 half nv = saturate(dot(normal, viewDir)); // TODO: this saturate should no be necessary here 24 #else 25 half nv = abs(dot(normal, viewDir)); // This abs allow to limit artifact 26 #endif
Part-2
1.计算NdotL/NdotH/LdotV/LdotH用于后续计算。
2.计算漫反射项,使用的是DisneyDiffuse函数,之后会提到。
3.计算粗糙度,使用PerceptualRoughnessToRoughness函数将感性粗糙度转换到学术意义上的粗糙度,上文已讲解。
4.选择V项和D项的不同实现函数,上文已讲解。
理论上,应该对diffuse项除π(见首段BRDF公式),并且不乘specularTerm。
但是——1.这会导致shader看起来比原来的颜色暗很多。2.在引擎看来,设置为Non-importance的灯光当加入ambient SH计算时需要除π。(ambient SH解释见后文)
所以,Unity中的diffuseTerm 计算,并没有除π,反而乘了NdotL。
1 half nl = saturate(dot(normal, light.dir)); 2 float nh = saturate(dot(normal, halfDir)); 3 4 half lv = saturate(dot(light.dir, viewDir)); 5 half lh = saturate(dot(light.dir, halfDir)); 6 7 // Diffuse term 8 half diffuseTerm = DisneyDiffuse(nv, nl, lh, perceptualRoughness) * nl; 9 10 // Specular term 11 // HACK: theoretically we should divide diffuseTerm by Pi and not multiply specularTerm! 12 // BUT 1) that will make shader look significantly darker than Legacy ones 13 // and 2) on engine side "Non-important" lights have to be divided by Pi too in cases when they are injected into ambient SH 14 float roughness = PerceptualRoughnessToRoughness(perceptualRoughness); 15 #if UNITY_BRDF_GGX 16 // GGX with roughtness to 0 would mean no specular at all, using max(roughness, 0.002) here to match HDrenderloop roughtness remapping. 17 roughness = max(roughness, 0.002); 18 half V = SmithJointGGXVisibilityTerm (nl, nv, roughness); 19 float D = GGXTerm (nh, roughness); 20 #else 21 // Legacy 22 half V = SmithBeckmannVisibilityTerm (nl, nv, roughness); 23 half D = NDFBlinnPhongNormalizedTerm (nh, PerceptualRoughnessToSpecPower(perceptualRoughness)); 24 #endif
DisneyDiffuse函数
输入:NdotV,NdotL,LdotH,感性粗糙度。
传统的漫反射计算使用的是Lambert模型,但是使用Lambert模型会使得物体的边缘过暗,和真实的表现有差异。因此,Disney的Diffuse计算采用了schlick的近似菲涅尔计算公式来弥补效果,公式见:
https://disney-animation.s3.amazonaws.com/library/s2012_pbs_disney_brdf_notes_v2.pdf
Page 14——section 5.3
在Unity的计算中,将公式的diffuseAlbedo / PI,即baseColor/π挪到函数外进行计算。
1 half DisneyDiffuse(half NdotV, half NdotL, half LdotH, half perceptualRoughness) 2 { 3 half fd90 = 0.5 + 2 * LdotH * LdotH * perceptualRoughness; 4 // Two schlick fresnel term 5 half lightScatter = (1 + (fd90 - 1) * Pow5(1 - NdotL)); 6 half viewScatter = (1 + (fd90 - 1) * Pow5(1 - NdotV)); 7 8 return lightScatter * viewScatter; 9 }
Part-3
BRDF1_Unity_PBS函数的最后一段代码,反而内容比较多。
1.计算高光项的一部分,菲涅尔项最后再添加。如果镜面反射高光关闭,那么镜面反射项为0。
2.如果开启了颜色空间GAMMA校正,那么这里会进行一次计算。
3.计算surfaceReduction参数。Unity在注释中给出了它的公式,但我并没有查到计算他的目的,在这里它用于间接光照的计算。
4.为了提供真正的Lambert照明,如果SpecColor的各个通道值均为0,那么就是全漫反射。
any - returns true if a boolean scalar or any component of a boolean vector is true.
5.最后的color输出,分为三个部分:漫反射+镜面反射+表面衰减。
漫反射:输入的漫反射颜色(纹理)*GI的漫反射颜色(间接光照)+输入的漫反射颜色(纹理)*光照颜色(直接光照)*漫反射项
镜面反射:镜面反射项(V项和D项)*光照颜色(直接光照)*菲涅尔项(F项)
表面衰减:表面衰减系数*GI镜面反射(间接光照)*菲涅尔插值
gi的类型为UnityIndirect结构体,解释见下文。
1 half specularTerm = V*D * UNITY_PI; // Torrance-Sparrow model, Fresnel is applied later 2 3 # ifdef UNITY_COLORSPACE_GAMMA 4 specularTerm = sqrt(max(1e-4h, specularTerm)); 5 # endif 6 7 // specularTerm * nl can be NaN on Metal in some cases, use max() to make sure it's a sane value 8 specularTerm = max(0, specularTerm * nl); 9 #if defined(_SPECULARHIGHLIGHTS_OFF) 10 specularTerm = 0.0; 11 #endif 12 13 // surfaceReduction = Int D(NdotH) * NdotH * Id(NdotL>0) dH = 1/(roughness^2+1) 14 half surfaceReduction; 15 # ifdef UNITY_COLORSPACE_GAMMA 16 surfaceReduction = 1.0-0.28*roughness*perceptualRoughness; // 1-0.28*x^3 as approximation for (1/(x^4+1))^(1/2.2) on the domain [0;1] 17 # else 18 surfaceReduction = 1.0 / (roughness*roughness + 1.0); // fade \in [0.5;1] 19 # endif 20 21 // To provide true Lambert lighting, we need to be able to kill specular completely. 22 specularTerm *= any(specColor) ? 1.0 : 0.0; 23 24 half grazingTerm = saturate(smoothness + (1-oneMinusReflectivity)); 25 half3 color = diffColor * (gi.diffuse + light.color * diffuseTerm) 26 + specularTerm * light.color * FresnelTerm (specColor, lh) 27 + surfaceReduction * gi.specular * FresnelLerp (specColor, grazingTerm, nv); 28 29 return half4(color, 1); 30 }
FresnelLerp函数
返回F0到F90之间的线性插值,t的实现和菲涅尔项中的实现一致。
1 inline half3 FresnelLerp (half3 F0, half3 F90, half cosA) 2 { 3 half t = Pow5 (1 - cosA); // ala Schlick interpoliation 4 return lerp (F0, F90, t); 5 }
计算oneMinusReflectivity的函数是EnergyConservationBetweenDiffuseAndSpecular,位于UnityStandardUtils.cginc。余下代码用在他处,暂不讨论。
1 inline half3 EnergyConservationBetweenDiffuseAndSpecular (half3 albedo, half3 specColor, out half oneMinusReflectivity) 2 { 3 oneMinusReflectivity = 1 - SpecularStrength(specColor); 4 #if !UNITY_CONSERVE_ENERGY 5 return albedo; 6 #elif UNITY_CONSERVE_ENERGY_MONOCHROME 7 return albedo * oneMinusReflectivity; 8 #else 9 return albedo * (half3(1,1,1) - specColor); 10 #endif 11 }
SpecularStrength函数
如果Shader Mode<3.0,返回镜面反射颜色的R通道。
否则,返回镜面反射颜色RGB三个通道中最大的通道值。
Note:Shader Mode 2.0由于指令数的限制,简化了这项运算,直接返回R通道值(因为大多数的金属要么是单色,要么是淡黄色/黄色调的,主要影响的是R通道)。
1 half SpecularStrength(half3 specular) 2 { 3 #if (SHADER_TARGET < 30) 4 // SM2.0: instruction count limitation 5 // SM2.0: simplified SpecularStrength 6 return specular.r; // Red channel - because most metals are either monocrhome or with redish/yellowish tint 7 #else 8 return max (max (specular.r, specular.g), specular.b); 9 #endif 10 }