本系列文章由@浅墨_毛星云 出品,转载请注明出处。
依然是附上一组本文配套工程的运行截图之后,便开始我们的正文。
二、续Standard Shader中正向基础渲染通道源码分析
此部分接上文《【浅墨Unity3D Shader编程】之十 深入理解Unity5中的Standard Shader(二)&屏幕油画特效的实现》的第二部分“Standard Shader中正向基础渲染通道源码分析“。
上文中分析了Standard Shader中正向基础渲染通道的源码,刚好分析完了顶点着色函数vertForwardBase,本文中将对片段着色函数fragForwardBase 进行说明。分析完之后,也就结束了这一系列长得稍微有些离谱的Standard Shader正向基础渲染通道的源码分析。OK,开始吧,先上注释好的片段着色函数fragForwardBase的代码,位于UnityStandardCore.cginc中:
//----------------------------------------【fragForwardBase函数】------------------------------------------- // 用途:正向渲染基础通道的片段着色函数 // 输入:VertexOutputForwardBase结构体 // 输出:一个half4类型的颜色值 //------------------------------------------------------------------------------------------------------------------ half4 fragForwardBase (VertexOutputForwardBase i) : SV_Target { //定义并初始化类型为FragmentCommonData的变量s FRAGMENT_SETUP(s) //若定义了UNITY_OPTIMIZE_TEXCUBELOD,则由输入的顶点参数来设置反射光方向向量 #if UNITY_OPTIMIZE_TEXCUBELOD s.reflUVW = i.reflUVW; #endif //设置主光照 UnityLight mainLight = MainLight (s.normalWorld); //设置阴影的衰减系数 half atten = SHADOW_ATTENUATION(i); //计算全局光照 half occlusion = Occlusion(i.tex.xy); UnityGI gi = FragmentGI (s, occlusion, i.ambientOrLightmapUV, atten, mainLight); //加上BRDF-基于物理的光照 half4 c = UNITY_BRDF_PBS (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect); //加上BRDF-全局光照 c.rgb += UNITY_BRDF_GI (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, occlusion, gi); //加上自发光 c.rgb += Emission(i.tex.xy); //设置雾效 UNITY_APPLY_FOG(i.fogCoord, c.rgb); //返回最终的颜色 return OutputForward (c, s.alpha); }依然是老规矩,把上面代码中新接触到的相关内容进行下分条讲解。
#define FRAGMENT_SETUP(x) FragmentCommonData x = \ FragmentSetup(i.tex, i.eyeVec, IN_VIEWDIR4PARALLAX(i), i.tangentToWorldAndParallax, IN_WORLDPOS(i));
FragmentCommonData x =FragmentSetup(i.tex, i.eyeVec, IN_VIEWDIR4PARALLAX(i), i.tangentToWorldAndParallax, IN_WORLDPOS(i));
其中FragmentSetup函数也定义于UnityStandardCore.cginc头文件中,用于填充一个FragmentCommonData结构体并于返回值中返回,也就是进行片段函数相关参数的初始化,相关代码如下:
//函数FragmentSetup:填充一个FragmentCommonData结构体并于返回值中返回,进行片段函数相关参数的初始化 inline FragmentCommonData FragmentSetup (float4 i_tex, half3 i_eyeVec, half3 i_viewDirForParallax, half4 tangentToWorld[3], half3 i_posWorld) { i_tex = Parallax(i_tex, i_viewDirForParallax); half alpha = Alpha(i_tex.xy); #if defined(_ALPHATEST_ON) clip (alpha - _Cutoff); #endif FragmentCommonData o = UNITY_SETUP_BRDF_INPUT (i_tex); o.normalWorld = PerPixelWorldNormal(i_tex, tangentToWorld); o.eyeVec = NormalizePerPixelNormal(i_eyeVec); o.posWorld = i_posWorld; // NOTE: shader relies on pre-multiply alpha-blend (_SrcBlend = One, _DstBlend = OneMinusSrcAlpha) o.diffColor = PreMultiplyAlpha (o.diffColor, alpha, o.oneMinusReflectivity, /*out*/ o.alpha); return o; }其中的FragmentCommonData结构体也是定义于UnityStandardCore.cginc头文件中:
//FragmentCommonData结构体:存放片段着色常用变量 struct FragmentCommonData { half3 diffColor, specColor;//漫反射颜色;镜面反射颜色 // Note: oneMinusRoughness & oneMinusReflectivity for optimization purposes, mostly for DX9 SM2.0 level. // Most of the math is being done on these (1-x) values, and that saves a few precious ALU slots. half oneMinusReflectivity, oneMinusRoughness;//1减去反射率;1减去粗糙度 half3 normalWorld, eyeVec, posWorld;//世界空间中的法线向量坐标;视角向量坐标;在世界坐标中的位置坐标 half alpha;//透明度 #if UNITY_OPTIMIZE_TEXCUBELOD || UNITY_STANDARD_SIMPLE half3 reflUVW;//反射率的UVW #endif #if UNITY_STANDARD_SIMPLE half3 tangentSpaceNormal;//切线空间中的法线向量 #endif };
2. MainLight函数
MainLight函数定义于UnityStandardCore.cginc头文件中,用途是实例化一个UnityLight结构体对象,并进行相应的填充,其返回值作为主光源。相关代码如下:
// 用途:该函数为主光照函数 // 说明:实例化一个UnityLight结构体对象,并进行相应的填充 /* //注:UnityLight结构体定义于UnityLightingCommon.cginc文件中,原型如下: struct UnityLight { half3 color; half3 dir; half ndotl; }; */ //------------------------------------【函数3】MainLight函数----------------------------------------- // 用途:该函数为主光照函数 // 说明:实例化一个UnityLight结构体对象,并进行相应的填充 //--------------------------------------------------------------------------------------------------------- UnityLight MainLight (half3 normalWorld) { //【1】实例化一个UnityLight的对象 UnityLight l; //【2】填充UnityLight的各个参数 //若光照贴图选项为关,使用Unity内置变量赋值 #ifdef LIGHTMAP_OFF //获取光源的颜色 l.color = _LightColor0.rgb; //获取光源的方向 l.dir = _WorldSpaceLightPos0.xyz; //获取法线与光源方向的点乘的积 l.ndotl = LambertTerm (normalWorld, l.dir); //光照贴图选项为开,将各项值设为0 #else l.color = half3(0.f, 0.f, 0.f); l.ndotl = 0.f; l.dir = half3(0.f, 0.f, 0.f); #endif //返回赋值完成的UnityLight结构体对象 return l; }
3. SHADOW_ATTENUATION宏
SHADOW_ATTENUATION宏相关的代码位于AutoLight.cginc头文件中,用于实现阴影渲染相关的辅助工作,代码如下:
// ---------------- // 阴影相关工具代码 || Shadow helpers // ---------------- // ---- 屏幕空间阴影 || Screen space shadows #if defined (SHADOWS_SCREEN) …… #define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord) #endif // ----聚光灯光源阴影 || Spot light shadows #if defined (SHADOWS_DEPTH) && defined (SPOT) #define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1; #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_World2Shadow[0], mul(_Object2World,v.vertex)); #define SHADOW_ATTENUATION(a) UnitySampleShadowmap(a._ShadowCoord) #endif // ----点光源阴影 || Point light shadows #if defined (SHADOWS_CUBE) #define SHADOW_COORDS(idx1) unityShadowCoord3 _ShadowCoord : TEXCOORD##idx1; #define TRANSFER_SHADOW(a) a._ShadowCoord = mul(_Object2World, v.vertex).xyz - _LightPositionRange.xyz; #define SHADOW_ATTENUATION(a) UnitySampleShadowmap(a._ShadowCoord) #endif // ---- 关闭阴影 || Shadows off #if !defined (SHADOWS_SCREEN) && !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE) #define SHADOW_COORDS(idx1) #define TRANSFER_SHADOW(a) #define SHADOW_ATTENUATION(a) 1.0 #endif
可以发现,SHADOW_ATTENUATION(a)宏除了在关闭阴影的状态是等于1以外,其他几种情况都是等价于UnitySampleShadowmap(a._ShadowCoord)函数的调用。而这里的UnitySampleShadowmap函数,定于于UnityShadowLibrary.cginc函数中。实现代码如下。
//------------------------------【UnitySampleShadowmap函数】--------------------------------- // 用途:采样阴影贴图,得到阴影衰减值 // 输入参数: float3型的阴影向量坐标vec // 返回值:阴影衰减值 //------------------------------------------------------------------------------------------------------- inline half UnitySampleShadowmap (float3 vec) { float mydist = length(vec) * _LightPositionRange.w; mydist *= 0.97; // bias #if defined (SHADOWS_SOFT) float z = 1.0/128.0; float4 shadowVals; shadowVals.x = SampleCubeDistance (vec+float3( z, z, z)); shadowVals.y = SampleCubeDistance (vec+float3(-z,-z, z)); shadowVals.z = SampleCubeDistance (vec+float3(-z, z,-z)); shadowVals.w = SampleCubeDistance (vec+float3( z,-z,-z)); half4 shadows = (shadowVals < mydist.xxxx) ? _LightShadowData.rrrr : 1.0f; return dot(shadows,0.25); #else float dist = SampleCubeDistance (vec); return dist < mydist ? _LightShadowData.r : 1.0; #endif }
4. Occlusion函数
Occlusion函数用于进行全局光照的第一步。其输入参数为一个float2型的纹理坐标,而其half型的返回值将作为FragmentGI函数的一个输入参数。Occlusion函数的原型如下:
half Occlusion(float2 uv) { #if (SHADER_TARGET < 30) // SM20: instruction count limitation // SM20: simpler occlusion return tex2D(_OcclusionMap, uv).g; #else half occ = tex2D(_OcclusionMap, uv).g; return LerpOneTo (occ, _OcclusionStrength); #endif }其中的LerpOneTo函数很简单,用于线性插值,输入两个值b和t,返回1+(b-1)*t,具体定义如下:
half LerpOneTo(half b, half t) { half oneMinusT = 1 - t; return oneMinusT + b * t; }
UnityGI结构体是Unity中存放全局光照光源信息的结构体,定义于UnityLightingCommon.cginc头文件中,如下。
//全局光照结构体 struct UnityGI { UnityLight light;//定义第一个光源参数结构体,表示第一个光源 //若定义了DIRLIGHTMAP_SEPARATE(单独的方向光源光照贴图) #ifdef DIRLIGHTMAP_SEPARATE //若定义了LIGHTMAP_ON(打开光照贴图) #ifdef LIGHTMAP_ON UnityLight light2;//定义第二个光源参数结构体,表示第二个光源 #endif //若定义了DYNAMICLIGHTMAP_ON(打开动态光照贴图) #ifdef DYNAMICLIGHTMAP_ON UnityLight light3;//定义第三个光源参数结构体,表示第三个光源 #endif #endif UnityIndirect indirect;//Unity中间接光源参数的结构体 };
其中包含了UnityLight结构体和UnityIndirect结构体,其中UnityLight结构体是Unity Shader中最基本的光照结构体,而UnityIndirect是Unity中存放间接光源信息的结构体。它们两者也定义于UnityLightingCommon.cginc头文件中,代码如下。
//Unity中光源参数的结构体 struct UnityLight { half3 color;//光源颜色 half3 dir;//光源方向 half ndotl; //入射光方向和当前表面法线方向的点积 }; //Unity中间接光源参数的结构体 struct UnityIndirect { half3 diffuse;//漫反射颜色 half3 specular;//镜面反射颜色 };
FragmentGI函数是片段着色部分全局光照的处理函数,定义于UnityStandardCore.cginc头文件中。相关代码如下:
//函数:片段着色部分全局光照的处理函数 inline UnityGI FragmentGI (FragmentCommonData s, half occlusion, half4 i_ambientOrLightmapUV, half atten, UnityLight light, bool reflections) { //【1】实例化一个UnityGIInput的对象 UnityGIInput d; //【2】填充此UnityGIInput对象的各个值 d.light = light; d.worldPos = s.posWorld; d.worldViewDir = -s.eyeVec; d.atten = atten; #if defined(LIGHTMAP_ON) || defined(DYNAMICLIGHTMAP_ON) d.ambient = 0; d.lightmapUV = i_ambientOrLightmapUV; #else d.ambient = i_ambientOrLightmapUV.rgb; d.lightmapUV = 0; #endif d.boxMax[0] = unity_SpecCube0_BoxMax; d.boxMin[0] = unity_SpecCube0_BoxMin; d.probePosition[0] = unity_SpecCube0_ProbePosition; d.probeHDR[0] = unity_SpecCube0_HDR; d.boxMax[1] = unity_SpecCube1_BoxMax; d.boxMin[1] = unity_SpecCube1_BoxMin; d.probePosition[1] = unity_SpecCube1_ProbePosition; d.probeHDR[1] = unity_SpecCube1_HDR; //【3】根据填充好的UnityGIInput结构体对象,调用一下UnityGlobalIllumination函数 if(reflections) { Unity_GlossyEnvironmentData g; g.roughness = 1 - s.oneMinusRoughness; #if UNITY_OPTIMIZE_TEXCUBELOD || UNITY_STANDARD_SIMPLE g.reflUVW = s.reflUVW; #else g.reflUVW = reflect(s.eyeVec, s.normalWorld); #endif return UnityGlobalIllumination (d, occlusion, s.normalWorld, g); } else { return UnityGlobalIllumination (d, occlusion, s.normalWorld); } } inline UnityGI FragmentGI (FragmentCommonData s, half occlusion, half4 i_ambientOrLightmapUV, half atten, UnityLight light) { return FragmentGI(s, occlusion, i_ambientOrLightmapUV, atten, light, true); }
其中的UnityGIInput结构体定义了全局光照所需要的一些函数,定义为如下:
//全局光照的输入参数结构体 struct UnityGIInput { UnityLight light; // 像素光源,由引擎准备并传输过来 || pixel light, sent from the engine float3 worldPos;//世界空间中的位置坐标 half3 worldViewDir;//世界空间中的视角方向向量坐标 half atten;//衰减值 half3 ambient;//环境光颜色 half4 lightmapUV; //光照贴图的UV坐标,其中 取.xy = static lightmapUV(静态光照贴图的UV) , .zw = dynamic lightmap UV(动态光照贴图的UV) float4 boxMax[2];//box最大值 float4 boxMin[2];//box最小值 float4 probePosition[2];//光照探针的位置 float4 probeHDR[2];//光照探针的高动态范围图像(High-Dynamic Range) };
FragmentGI函数最终利用了UnityGlobalIllumination函数,其定义于UnityGlobalIllumination.cginc头文件中,实现如下。
inline UnityGI UnityGlobalIllumination (UnityGIInput data, half occlusion, half3 normalWorld) { return UnityGI_Base(data, occlusion, normalWorld); }
//UnityGI_Base函数:Unity的全局光照Base版 inline UnityGI UnityGI_Base(UnityGIInput data, half occlusion, half3 normalWorld) { //【1】实例化一个UnityGI类型的结构体 UnityGI o_gi; //【2】重置此UnityGI的结构体 ResetUnityGI(o_gi); //【3】开始逐个填充参数 #if !defined(LIGHTMAP_ON) o_gi.light = data.light; o_gi.light.color *= data.atten; #endif #if UNITY_SHOULD_SAMPLE_SH #if UNITY_SAMPLE_FULL_SH_PER_PIXEL half3 sh = ShadeSH9(half4(normalWorld, 1.0)); #elif (SHADER_TARGET >= 30) && !UNITY_STANDARD_SIMPLE half3 sh = data.ambient + ShadeSH12Order(half4(normalWorld, 1.0)); #else half3 sh = data.ambient; #endif o_gi.indirect.diffuse = sh; #endif #if defined(LIGHTMAP_ON) // Baked lightmaps fixed4 bakedColorTex = UNITY_SAMPLE_TEX2D(unity_Lightmap, data.lightmapUV.xy); half3 bakedColor = DecodeLightmap(bakedColorTex); #ifdef DIRLIGHTMAP_OFF o_gi.indirect.diffuse = bakedColor; #ifdef SHADOWS_SCREEN o_gi.indirect.diffuse = MixLightmapWithRealtimeAttenuation (o_gi.indirect.diffuse, data.atten, bakedColorTex); #endif // SHADOWS_SCREEN #elif DIRLIGHTMAP_COMBINED fixed4 bakedDirTex = UNITY_SAMPLE_TEX2D_SAMPLER (unity_LightmapInd, unity_Lightmap, data.lightmapUV.xy); o_gi.indirect.diffuse = DecodeDirectionalLightmap (bakedColor, bakedDirTex, normalWorld); #ifdef SHADOWS_SCREEN o_gi.indirect.diffuse = MixLightmapWithRealtimeAttenuation (o_gi.indirect.diffuse, data.atten, bakedColorTex); #endif // SHADOWS_SCREEN #elif DIRLIGHTMAP_SEPARATE // Left halves of both intensity and direction lightmaps store direct light; right halves - indirect. // Direct fixed4 bakedDirTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_LightmapInd, unity_Lightmap, data.lightmapUV.xy); o_gi.indirect.diffuse = DecodeDirectionalSpecularLightmap (bakedColor, bakedDirTex, normalWorld, false, 0, o_gi.light); // Indirect half2 uvIndirect = data.lightmapUV.xy + half2(0.5, 0); bakedColor = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, uvIndirect)); bakedDirTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_LightmapInd, unity_Lightmap, uvIndirect); o_gi.indirect.diffuse += DecodeDirectionalSpecularLightmap (bakedColor, bakedDirTex, normalWorld, false, 0, o_gi.light2); #endif #endif #ifdef DYNAMICLIGHTMAP_ON // Dynamic lightmaps fixed4 realtimeColorTex = UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, data.lightmapUV.zw); half3 realtimeColor = DecodeRealtimeLightmap (realtimeColorTex); #ifdef DIRLIGHTMAP_OFF o_gi.indirect.diffuse += realtimeColor; #elif DIRLIGHTMAP_COMBINED half4 realtimeDirTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_DynamicDirectionality, unity_DynamicLightmap, data.lightmapUV.zw); o_gi.indirect.diffuse += DecodeDirectionalLightmap (realtimeColor, realtimeDirTex, normalWorld); #elif DIRLIGHTMAP_SEPARATE half4 realtimeDirTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_DynamicDirectionality, unity_DynamicLightmap, data.lightmapUV.zw); half4 realtimeNormalTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_DynamicNormal, unity_DynamicLightmap, data.lightmapUV.zw); o_gi.indirect.diffuse += DecodeDirectionalSpecularLightmap (realtimeColor, realtimeDirTex, normalWorld, true, realtimeNormalTex, o_gi.light3); #endif #endif o_gi.indirect.diffuse *= occlusion; //【4】返回此UnityGI类型的结构体 return o_gi; }不难发现,此FragmentGI函数的实现,也就是实例化一个UnityGIInput结构体对象,然后依次填充了此结构体对象的每个各个参数,最后调用一下基于UnityGI_Base函数的UnityGlobalIllumination函数而已。
首先,这边有一段宏,定义于UnityPBSLighting.cginc头文件中,根据不同的情况,将UNITY_BRDF_PBS宏定义为不同版本的UNITY_BRDF_PBS宏——是BRDF3_Unity_PBS、BRDF2_Unity_PBS还是BRDF1_Unity_PBS。
//------------------------------------------------------------------------------------- // 默认使用BRDF || Default BRDF to use: #if !defined (UNITY_BRDF_PBS) // 允许显式地在自定义着色器中重写BRDF的实现细节 || allow to explicitly override BRDF in custom shader //满足着色目标模型的版本小于Shader Model 3.0,或者是PlayStation 2平台 #if (SHADER_TARGET < 30) || defined(SHADER_API_PSP2) // 为小于SM3.0的着色模型回退为低保真度的BRDF版本 || Fallback to low fidelity one for pre-SM3.0 #define UNITY_BRDF_PBS BRDF3_Unity_PBS #elif defined(SHADER_API_MOBILE) // 为移动平台简化的BRDF版本 || Somewhat simplified for mobile #define UNITY_BRDF_PBS BRDF2_Unity_PBS #else //最高特效的SM3、PC平台或者游戏主机平台的BRDF版本 || Full quality for SM3+ PC / consoles #define UNITY_BRDF_PBS BRDF1_Unity_PBS #endif #endif三种情况下,BRDF3_Unity_PBS、BRDF2_Unity_PBS、 BRDF1_Unity_PBS三个函数的参数和返回值都一样,区别仅仅是内部的实现。在这边,以BRDF1_Unity_PBS为例,讲一下参数值的含义。
第七个参数,UnityLight型的light,表示Unity中光源参数的结构体,包含half3型的光源颜色color,half3型的光源方向dir,half型的入射光方向和当前表面法线方向的点乘的积ndotl。上文有贴出过其实现代码,都几次提到了,这边就再贴一下。
struct UnityLight { half3 color;//光源颜色 half3 dir;//光源方向 half ndotl; //入射光方向和当前表面法线方向的点积 };第八个参数,UnityIndirect类型的gi ,一个包含了half3型的漫反射颜色diffuse和half3型的镜面反射颜色specular的光线反射结构体, 表示间接光照信息 。
struct UnityIndirect { half3 diffuse;//漫反射颜色 half3 specular;//镜面反射颜色 };
下面将三种版本的函数分别贴出来,它们都定义于UnityStandardBRDF.cginc头文件中。
//最高特效的SM3、PC平台或者游戏主机平台的BRDF版本 || Full quality for SM3+ PC / consoles //------------------------------------------------------------------------------------- // Note: BRDF entry points use oneMinusRoughness (aka "smoothness") and oneMinusReflectivity for optimization // purposes, mostly for DX9 SM2.0 level. Most of the math is being done on these (1-x) values, and that saves // a few precious ALU slots. // Main Physically Based BRDF // Derived from Disney work and based on Torrance-Sparrow micro-facet model // // BRDF = kD / pi + kS * (D * V * F) / 4 // I = BRDF * NdotL // // * NDF (depending on UNITY_BRDF_GGX): // a) Normalized BlinnPhong // b) GGX // * Smith for Visiblity term // * Schlick approximation for Fresnel half4 BRDF1_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half oneMinusRoughness, half3 normal, half3 viewDir, UnityLight light, UnityIndirect gi) { half roughness = 1-oneMinusRoughness; half3 halfDir = Unity_SafeNormalize (light.dir + viewDir); half nl = light.ndotl; half nh = BlinnTerm (normal, halfDir); half nv = DotClamped (normal, viewDir); half lv = DotClamped (light.dir, viewDir); half lh = DotClamped (light.dir, halfDir); #if UNITY_BRDF_GGX half V = SmithGGXVisibilityTerm (nl, nv, roughness); half D = GGXTerm (nh, roughness); #else half V = SmithBeckmannVisibilityTerm (nl, nv, roughness); half D = NDFBlinnPhongNormalizedTerm (nh, RoughnessToSpecPower (roughness)); #endif half nlPow5 = Pow5 (1-nl); half nvPow5 = Pow5 (1-nv); half Fd90 = 0.5 + 2 * lh * lh * roughness; half disneyDiffuse = (1 + (Fd90-1) * nlPow5) * (1 + (Fd90-1) * nvPow5); // HACK: theoretically we should divide by Pi diffuseTerm and not multiply specularTerm! // BUT 1) that will make shader look significantly darker than Legacy ones // and 2) on engine side "Non-important" lights have to be divided by Pi to in cases when they are injected into ambient SH // NOTE: multiplication by Pi is part of single constant together with 1/4 now half specularTerm = max(0, (V * D * nl) * unity_LightGammaCorrectionConsts_PIDiv4);// Torrance-Sparrow model, Fresnel is applied later (for optimization reasons) half diffuseTerm = disneyDiffuse * nl; half grazingTerm = saturate(oneMinusRoughness + (1-oneMinusReflectivity)); half3 color = diffColor * (gi.diffuse + light.color * diffuseTerm) + specularTerm * light.color * FresnelTerm (specColor, lh) + gi.specular * FresnelLerp (specColor, grazingTerm, nv); return half4(color, 1); }
// 为移动平台简化的BRDF版本 || Somewhat simplified for mobile // Based on Minimalist CookTorrance BRDF // Implementation is slightly different from original derivation: http://www.thetenthplanet.de/archives/255 // // * BlinnPhong as NDF // * Modified Kelemen and Szirmay-Kalos for Visibility term // * Fresnel approximated with 1/LdotH half4 BRDF2_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half oneMinusRoughness, half3 normal, half3 viewDir, UnityLight light, UnityIndirect gi) { half3 halfDir = Unity_SafeNormalize (light.dir + viewDir); half nl = light.ndotl; half nh = BlinnTerm (normal, halfDir); half nv = DotClamped (normal, viewDir); half lh = DotClamped (light.dir, halfDir); half roughness = 1-oneMinusRoughness; half specularPower = RoughnessToSpecPower (roughness); // Modified with approximate Visibility function that takes roughness into account // Original ((n+1)*N.H^n) / (8*Pi * L.H^3) didn't take into account roughness // and produced extremely bright specular at grazing angles // HACK: theoretically we should divide by Pi diffuseTerm and not multiply specularTerm! // BUT 1) that will make shader look significantly darker than Legacy ones // and 2) on engine side "Non-important" lights have to be divided by Pi to in cases when they are injected into ambient SH // NOTE: multiplication by Pi is cancelled with Pi in denominator half invV = lh * lh * oneMinusRoughness + roughness * roughness; // approx ModifiedKelemenVisibilityTerm(lh, 1-oneMinusRoughness); half invF = lh; half specular = ((specularPower + 1) * pow (nh, specularPower)) / (unity_LightGammaCorrectionConsts_8 * invV * invF + 1e-4h); // @TODO: might still need saturate(nl*specular) on Adreno/Mali // Prevent FP16 overflow on mobiles #if SHADER_API_GLES || SHADER_API_GLES3 specular = clamp(specular, 0.0, 100.0); #endif half grazingTerm = saturate(oneMinusRoughness + (1-oneMinusReflectivity)); half3 color = (diffColor + specular * specColor) * light.color * nl + gi.diffuse * diffColor + gi.specular * FresnelLerpFast (specColor, grazingTerm, nv); return half4(color, 1); }
// 为小于SM3.0的着色模型回退为低保真度的BRDF版本 || Fallback to low fidelity one for pre-SM3.0 // Old school, not microfacet based Modified Normalized Blinn-Phong BRDF // Implementation uses Lookup texture for performance // // * Normalized BlinnPhong in RDF form // * Implicit Visibility term // * No Fresnel term // // TODO: specular is too weak in Linear rendering mode half4 BRDF3_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half oneMinusRoughness, half3 normal, half3 viewDir, UnityLight light, UnityIndirect gi) { half3 reflDir = reflect (viewDir, normal); half nl = light.ndotl; half nv = DotClamped (normal, viewDir); // Vectorize Pow4 to save instructions half2 rlPow4AndFresnelTerm = Pow4 (half2(dot(reflDir, light.dir), 1-nv)); // use R.L instead of N.H to save couple of instructions half rlPow4 = rlPow4AndFresnelTerm.x; // power exponent must match kHorizontalWarpExp in NHxRoughness() function in GeneratedTextures.cpp half fresnelTerm = rlPow4AndFresnelTerm.y; half grazingTerm = saturate(oneMinusRoughness + (1-oneMinusReflectivity)); half3 color = BRDF3_Direct(diffColor, specColor, rlPow4, oneMinusRoughness); color *= light.color * nl; color += BRDF3_Indirect(diffColor, specColor, gi, grazingTerm, fresnelTerm); return half4(color, 1); }
BRDF1_Unity_PBS函数的实现部分用到了最多的变量,最终表现效果最好,主要用于Shader Model 3.0、PC平台或者游戏主机平台。BRDF2_Unity_PBS简化了一部分计算,主要用于移动平台,而BRDF3_Unity_PBS是为Shader Model 小于3.0的着色模型提供基本版的BRDF,实现细节最为简陋。
8. UNITY_BRDF_GI宏
UNITY_BRDF_GI宏位于UnityPBSLighting.cginc头文件中,相关代码如下。
//------------------------------------------------------------------------------------- // 从间接的方向光照贴图中进行BRDF(双向反射分布函数)的光照提取 || BRDF for lights extracted from *indirect* directional lightmaps (baked and realtime). // 使用UNITY_BRDF_PBS从方向光源烘焙方向光照贴图, || Baked directional lightmap with *direct* light uses UNITY_BRDF_PBS. // 若想得到更好的效果,可以使用BRDF1_Unity_PBS || For better quality change to BRDF1_Unity_PBS. // SM2.0中的非方向光照贴图|| No directional lightmaps in SM2.0. //若没有定义UNITY_BRDF_PBS_LIGHTMAP_INDIRECT宏 #if !defined(UNITY_BRDF_PBS_LIGHTMAP_INDIRECT) //定义UNITY_BRDF_PBS_LIGHTMAP_INDIRECT = BRDF2_Unity_PBS #define UNITY_BRDF_PBS_LIGHTMAP_INDIRECT BRDF2_Unity_PBS #endif //若没有定义UNITY_BRDF_GI宏 #if !defined (UNITY_BRDF_GI) //定义UNITY_BRDF_GI = BRDF_Unity_Indirect #define UNITY_BRDF_GI BRDF_Unity_Indirect #endif
//间接光照的BRDF inline half3 BRDF_Unity_Indirect (half3 baseColor, half3 specColor, half oneMinusReflectivity, half oneMinusRoughness, half3 normal, half3 viewDir, half occlusion, UnityGI gi) { half3 c = 0; #if defined(DIRLIGHTMAP_SEPARATE) gi.indirect.diffuse = 0; gi.indirect.specular = 0; #ifdef LIGHTMAP_ON c += UNITY_BRDF_PBS_LIGHTMAP_INDIRECT (baseColor, specColor, oneMinusReflectivity, oneMinusRoughness, normal, viewDir, gi.light2, gi.indirect).rgb * occlusion; #endif #ifdef DYNAMICLIGHTMAP_ON c += UNITY_BRDF_PBS_LIGHTMAP_INDIRECT (baseColor, specColor, oneMinusReflectivity, oneMinusRoughness, normal, viewDir, gi.light3, gi.indirect).rgb * occlusion; #endif #endif return c; }
Emission函数定于于UnityStandardInput.cginc头文件中,根据指定的自发光光照贴图,利用tex2D函数,对输入的纹理进行光照贴图的采样,相关代码如下:
//---------------------------------------【Emission函数】----------------------------------------- // 用途:根据指定的自发光光照贴图,利用tex2D函数,对输入的纹理进行光照贴图的采样 // 输入参数:float2型的纹理坐标 // 输出参数:经过将自发光纹理和输入纹理进行tex2D采样得到的half3型的自发光颜色 //----------------------------------------------------------------------------------------------- half3 Emission(float2 uv) { #ifndef _EMISSION return 0; #else return tex2D(_EmissionMap, uv).rgb * _EmissionColor.rgb; #endif }
sampler2D _EmissionMap;
//自发光纹理图 _EmissionMap("Emission", 2D) = "white" {}
UNITY_APPLY_FOG宏相关的一些代码用于雾效的启用与否的辅助工作,定义于UnityCG.cginc头文件中,这边贴出注释好的代码即可。
//UNITY_FOG_LERP_COLOR宏的定义 #define UNITY_FOG_LERP_COLOR(col,fogCol,fogFac) col.rgb = lerp((fogCol).rgb, (col).rgb, saturate(fogFac)) //【1】若已经定义了FOG_LINEAR、FOG_EXP、FOG_EXP2宏三者至少之一,便可以进行到此#if实现部分 #if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2) //【1-1】若满足着色目标模型的版本小于Shader Model 3.0,或者定义了SHADER_API_MOBILE宏,便可以进行到此#if实现部分 #if (SHADER_TARGET < 30) || defined(SHADER_API_MOBILE) //移动平台和Shader Model 2.0:已经计算了每顶点的雾效因子,所以插一下值就可以了 ||mobile or SM2.0: fog factor was already calculated per-vertex, so just lerp the color //定义 UNITY_APPLY_FOG_COLOR(coord,col,fogCol) 等价于UNITY_FOG_LERP_COLOR(col,fogCol,coord) #define UNITY_APPLY_FOG_COLOR(coord,col,fogCol) UNITY_FOG_LERP_COLOR(col,fogCol,coord) //【1-2】 Shader Model 3.0和PC/游戏主机平台:计算雾效因子以及进行雾颜色的插值 ||SM3.0 and PC/console: calculate fog factor and lerp fog color #else //定义 UNITY_APPLY_FOG_COLOR(coord,col,fogCol)等价于UNITY_CALC_FOG_FACTOR(coord); UNITY_FOG_LERP_COLOR(col,fogCol,unityFogFactor) #define UNITY_APPLY_FOG_COLOR(coord,col,fogCol) UNITY_CALC_FOG_FACTOR(coord); UNITY_FOG_LERP_COLOR(col,fogCol,unityFogFactor) #endif //【2】否则,直接定义UNITY_APPLY_FOG_COLOR宏 #else #define UNITY_APPLY_FOG_COLOR(coord,col,fogCol) #endif //【3】若定义了UNITY_PASS_FORWARDADD(正向附加渲染通道)宏 #ifdef UNITY_PASS_FORWARDADD //定义UNITY_APPLY_FOG(coord,col) 等价于UNITY_APPLY_FOG_COLOR(coord,col,fixed4(0,0,0,0)) #define UNITY_APPLY_FOG(coord,col) UNITY_APPLY_FOG_COLOR(coord,col,fixed4(0,0,0,0)) //【4】否则,UNITY_APPLY_FOG(coord,col) 等价于 UNITY_APPLY_FOG_COLOR(coord,col,unity_FogColor) #else #define UNITY_APPLY_FOG(coord,col) UNITY_APPLY_FOG_COLOR(coord,col,unity_FogColor) #endif
//-----------------------------【函数OutputForward】---------------------------------------------- // 用途:正向渲染通道输出函数 // 输入参数:一个half4类型的一个颜色值output,一个half型的透明度值alphaFromSurface // 返回值:经过透明处理的half4型的输出颜色值 //------------------------------------------------------------------------------------------------- half4 OutputForward (half4 output, half alphaFromSurface) { #if defined(_ALPHABLEND_ON) || defined(_ALPHAPREMULTIPLY_ON) output.a = alphaFromSurface; #else UNITY_OPAQUE_ALPHA(output.a); #endif return output; }
其中UNITY_OPAQUE_ALPHA宏的定义为:
#define UNITY_OPAQUE_ALPHA(outputAlpha) outputAlpha = 1.0
我们都知道,Unity中的屏幕特效通常分为两部分来实现:
Shader "浅墨Shader编程/Volume11/PixelEffect" { //------------------------------------【属性值】------------------------------------ Properties { //主纹理 _MainTex("Texture", 2D) = "white" {} //封装的变量值 _Params("PixelNumPerRow (X) Ratio (Y)", Vector) = (80, 1, 1, 1.5) } //------------------------------------【唯一的子着色器】------------------------------------ SubShader { //关闭剔除操作 Cull Off //关闭深度写入模式 ZWrite Off //设置深度测试模式:渲染所有像素.等同于关闭透明度测试(AlphaTest Off) ZTest Always //--------------------------------唯一的通道------------------------------- Pass { //===========开启CG着色器语言编写模块=========== CGPROGRAM //编译指令:告知编译器顶点和片段着色函数的名称 #pragma vertex vert #pragma fragment frag //包含头文件 #include "UnityCG.cginc" //顶点着色器输入结构 struct vertexInput { float4 vertex : POSITION;//顶点位置 float2 uv : TEXCOORD0;//一级纹理坐标 }; //顶点着色器输出结构 struct vertexOutput { float4 vertex : SV_POSITION;//像素位置 float2 uv : TEXCOORD0;//一级纹理坐标 }; //--------------------------------【顶点着色函数】----------------------------- // 输入:顶点输入结构体 // 输出:顶点输出结构体 //--------------------------------------------------------------------------------- //顶点着色函数 vertexOutput vert(vertexInput v) { //【1】实例化一个输入结构体 vertexOutput o; //【2】填充此输出结构 //输出的顶点位置(像素位置)为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口 o.vertex = mul(UNITY_MATRIX_MVP, v.vertex); //输入的UV纹理坐标为顶点输出的坐标 o.uv = v.uv; //【3】返回此输出结构对象 return o; } //变量的声明 sampler2D _MainTex; half4 _Params; //进行像素化操作的自定义函数PixelateOperation half4 PixelateOperation(sampler2D tex, half2 uv, half scale, half ratio) { //【1】计算每个像素块的尺寸 half PixelSize = 1.0 / scale; //【2】取整计算每个像素块的坐标值,ceil函数,对输入参数向上取整 half coordX=PixelSize * ceil(uv.x / PixelSize); half coordY = (ratio * PixelSize)* ceil(uv.y / PixelSize / ratio); //【3】组合坐标值 half2 coord = half2(coordX,coordY); //【4】返回坐标值 return half4(tex2D(tex, coord).xyzw); } //--------------------------------【片段着色函数】----------------------------- // 输入:顶点输出结构体 // 输出:float4型的像素颜色值 //--------------------------------------------------------------------------------- fixed4 frag(vertexOutput Input) : COLOR { //使用自定义的PixelateOperation函数,计算每个像素经过取整后的颜色值 return PixelateOperation(_MainTex, Input.uv, _Params.x, _Params.y); } //===========结束CG着色器语言编写模块=========== ENDCG } } }
//进行像素化操作的自定义函数PixelateOperation half4 PixelateOperation(sampler2D tex, half2 uv, half scale, half ratio) { //【1】计算每个像素块的尺寸 half PixelSize = 1.0 / scale; //【2】取整计算每个像素块的坐标值,ceil函数,对输入参数向上取整 half coordX=PixelSize * ceil(uv.x / PixelSize); half coordY=( ratio * PixelSize ) * ceil(uv.y / PixelSize / ratio); //【3】组合坐标值 half2 coord = half2(coordX,coordY); //【4】返回坐标值 return half4(tex2D(tex, coord).xyzw); }
然后在片段着色器中调用此自定义的PixelateOperation函数,其返回值就作为片段函数frag的返回值即可:
fixed4 frag(vertexOutput Input) : COLOR { //使用自定义的PixelateOperation函数,计算每个像素经过取整后的颜色值 return PixelateOperation(_MainTex, Input.uv, _Params.x, _Params.y); }
C#脚本文件的代码依然是几乎从之前的几个特效中重用,只用稍微改一点细节就可以。贴出详细注释的实现此特效的C#脚本:
using UnityEngine; using System.Collections; //设置在编辑模式下也执行该脚本 [ExecuteInEditMode] //添加选项到菜单中 [AddComponentMenu("浅墨Shader编程/Volume11/PixelEffect")] public class PixelEffect : MonoBehaviour { //-----------------------------变量声明部分--------------------------- #region Variables //着色器和材质实例 public Shader CurShader; private Material CurMaterial; //三个可调节的自定义参数 [Range(1f, 1024f), Tooltip("屏幕每行将被均分为多少个像素块")] public float PixelNumPerRow = 580.0f; [Tooltip("自动计算平方像素所需的长宽比与否")] public bool AutoCalulateRatio = true; [Range(0f, 24f), Tooltip("此参数用于自定义长宽比")] public float Ratio = 1.0f; #endregion //-------------------------材质的get&set---------------------------- #region MaterialGetAndSet Material material { get { if(CurMaterial == null) { CurMaterial = new Material(CurShader); CurMaterial.hideFlags = HideFlags.HideAndDontSave; } return CurMaterial; } } #endregion //-----------------------------------------【Start()函数】--------------------------------------------- // 说明:此函数仅在Update函数第一次被调用前被调用 //-------------------------------------------------------------------------------------------------------- void Start () { //找到当前的Shader文件 CurShader = Shader.Find("浅墨Shader编程/Volume11/PixelEffect"); //判断当前设备是否支持屏幕特效 if(!SystemInfo.supportsImageEffects) { enabled = false; return; } } //-------------------------------------【OnRenderImage()函数】------------------------------------ // 说明:此函数在当完成所有渲染图片后被调用,用来渲染图片后期效果 //-------------------------------------------------------------------------------------------------------- void OnRenderImage (RenderTexture sourceTexture, RenderTexture destTexture) { //着色器实例不为空,就进行参数设置 if(CurShader != null) { float pixelNumPerRow = PixelNumPerRow; //给Shader中的外部变量赋值 material.SetVector("_Params", new Vector2(pixelNumPerRow, AutoCalulateRatio ? ((float)sourceTexture.width / (float)sourceTexture.height) : Ratio )); Graphics.Blit(sourceTexture, destTexture, material); } //着色器实例为空,直接拷贝屏幕上的效果。此情况下是没有实现屏幕特效的 else { //直接拷贝源纹理到目标渲染纹理 Graphics.Blit(sourceTexture, destTexture); } } //-----------------------------------------【Update()函数】---------------------------------------- // 说明:此函数在每一帧中都会被调用 //------------------------------------------------------------------------------------------------------ void Update() { //若程序在运行,进行赋值 if (Application.isPlaying) { #if UNITY_EDITOR if (Application.isPlaying != true) { CurShader = Shader.Find("浅墨Shader编程/Volume11/PixelEffect"); } #endif } } //-----------------------------------------【OnDisable()函数】--------------------------------------- // 说明:当对象变为不可用或非激活状态时此函数便被调用 //-------------------------------------------------------------------------------------------------------- void OnDisable () { if(CurMaterial) { //立即销毁材质实例 DestroyImmediate(CurMaterial); } } }根据我们C#脚本中参数的设定,可以有每行每列的像素个数PixelNumPerRow参数、是否自动计算正方形像素所需的长宽比与否AutoCalulateRatio参数、自定义长宽比的Ratio参数可以调节。而需要注意,若AutoCalulateRatio参数被勾选,我们的Shader将自动计算正方形像素所需的长宽比,这样第三个参数Ratio也就失效了。反正,若AutoCalulateRatio参数没有被勾选,就可以用Ratio参数自己定制像素的长宽比。
至此,本文结束。感谢大家的捧场,我们下次更新再会。
PS:最近一段时间临近硕士毕业,有不少学业方面的事情需要处理,博客得停更一段时间,请见谅。
附: 本博文相关下载链接清单