找回密码
 立即注册
查看: 806|回复: 7

[笔记] Unity PBR Standard Shader 实现详解 (三)全局光照函数计算

[复制链接]
发表于 2021-11-20 20:24 | 显示全部楼层 |阅读模式
这篇文章是系列文章,具体讲了PBR Shader在Unity下从原理到实现的整个过程。需要回顾的朋友可以看一下前两篇:
这一篇接着上一篇的内容。上一篇《Shader框架和数据准备》中,我们写了一个完整的shader,但这个shader的主要内容并不是PBR计算,而是计算前的数据准备,在shader的最后,我们使用了两个方法
//基于PBS的全局光照(gi变量)的计算函数。计算结果是gi的参数(Light参数和Indirect参数)。注意这一步还没有做真的光照计算。
LightingStandard_GI(o, giInput, gi);
fixed4 c = 0;
// realtime lighting: call lighting function
//PBS计算
c += LightingStandard(o, worldViewDir, gi);这两个方法是最终光照计算的主要内容,尤其是第二个方法,是BRDF计算的主干。但我们一个个来,今天先处理第一个方法LightingStandard_GI,这个方法计算了PBS后续计算中所需要的全局光照数据。
因为这个部分其实都是Unity内置的cginc文件的讲解,所以不需要写shader,但可以自己写一个cginc去测试这些流程。
因为Untiy的各种分支都错综复杂,函数的各种调用有如迷宫,这里我给一个流程图,读者可以留存,边看文章边看流程,就明白执行在了何处:


话不多说,接下来点赞上车
1.LightingStandard_GI函数

看看这个函数长什么样子
inline void LightingStandard_GI (SurfaceOutputStandard s,UnityGIInput data,inout UnityGI gi)//伪代码
{
    //如果是延迟渲染pass并且开启反射缓存(反射探针用延迟渲染的方式)
    #if defined(UNITY_PASS_DEFERRED) && UNITY_ENABLE_REFLECTION_BUFFERS
        gi = UnityGlobalIllumination(data, s.Occlusion, s.Normal);//注意这里是3个变量
    #else//这里是正常走的前向渲染通道。
        //UnityGlossyEnvironmentData是反射环境输入数据。(roughness和reflUVW)
        Unity_GlossyEnvironmentData g = UnityGlossyEnvironmentSetup(s.Smoothness, data.worldViewDir, s.Normal, lerp(unity_ColorSpaceDielectricSpec.rgb, s.Albedo, s.Metallic));
        gi = UnityGlobalIllumination(data, s.Occlusion, s.Normal, g);//这里是4个变量(giInput,AO,worldNormal,)
    #endif
}首先函数返回值是void,也就没有返回值,所有的骚操作都放在了inout的gi变量里。输入了SurfaceOutputStandard s,UnityGIInput data,UnityGI gi三个变量,那这三个变量的准备我在上一篇文章里有详细的解释了。
函数里面有两个分支,第一个分支是延迟渲染路径。由于日常使用还是前向渲染多,所以我们先跳过。不过这里UnityGlobalIllumination()有一个函数重载的方法,就是不同的变量数量和类型导向不同的函数分支。可以了解一下。
第二个分支是前向渲染路径,首先是UnityGlossyEnvironmentSetup
我们先看一下它的返回值类型Unity_GlossyEnvironmentData,在cginc文件里可以找到
struct Unity_GlossyEnvironmentData
{
    // - Deferred case have one cubemap//延迟渲染只有一个反射探针
    // - Forward case can have two blended cubemap (unusual should be deprecated(弃用)).//前向渲染可以有两个反射探针相互混合
    // Surface properties use for cubemap integration用于cubemap积分的表面属性
    half    roughness; // CAUTION: This is perceptualRoughness(感觉上的roughness) but because of compatibility(兼容性) this name can't be change :(
    half3   reflUVW;//反射环境cubemap采样坐标
};注释很多,实际上就是为cubemap结合roughness采样做准备。这里有两个变量:roughness粗糙度,三通道的reflUVW反射采样坐标。
那么UnityGlossyEnvironmentSetup就是获得这两个值的函数,我们看一下:
Unity_GlossyEnvironmentData UnityGlossyEnvironmentSetup(half Smoothness, half3 worldViewDir, half3 Normal, half3 fresnel0)//伪代码
{
    Unity_GlossyEnvironmentData g;//声明变量
    g.roughness /*perceptualRoughness*/=SmoothnessToPerceptualRoughness(Smoothness);//= 1 - s.smoothness
    g.reflUVW   = reflect(-worldViewDir, Normal);//常规计算反射Cupmap采样坐标的方法
    return g;
}里面的计算内容也不复杂,roughness = 1-smoothness,smoothness是之前在贴图里的采样值。
反射采样坐标的计算需要理解何谓反射坐标。其实反射环境,也就是天空盒或者cubemap,本质上都是一个正方体的盒子,上面有贴图。这里我画个渣渣草图示意一下



反射向量始终会指向天空盒上的一个点,那么就可以拿反射向量来作为坐标采样cubemap。
我们已经拥有了viewDir和normal。而HLSL中的reflect函数是通过入射方向和法线方向求反射方向的,那么入射方向其实就是viewDir的反方向-viewDir。所以有
g.reflUVW   = reflect(-worldViewDir, Normal);//常规计算反射Cupmap采样坐标的方法那么到这里UnityGlossyEnvironmentSetup就执行完毕,返回一个roughness值和reflUVW
2.UnityGlobalIllumination函数及其中的UnityGI_Base函数(计算间接光照Diffuse部分数据(BRDF前))

LightingStandard_GI包中的第二个函数是UnityGlobalIllumination函数,在第一个函数准备好Unity_GlossyEnvironmentData g后,就进入这个函数计算
我们先看一下函数的本体:
inline UnityGI UnityGlobalIllumination (UnityGIInput data, half occlusion, half3 normalWorld, Unity_GlossyEnvironmentData glossIn)
{
    //声明要返回的UniytGI,这个UnityGI_Base只计算gi.indirect.diffuse(带AO),不计算specular。   
    UnityGI o_gi = UnityGI_Base(data, occlusion, normalWorld);
    o_gi.indirect.specular = UnityGI_IndirectSpecular(data, occlusion, glossIn);//这个部分计算Specular
    return o_gi;
}返回类型 UnityGI 这个我在这个系列的上篇文章中有详细说,这里不赘述,主要就是直接光照和间接光照信息。
输入方面,UnityGIInput上篇文章也有提,主要是灯光的信息。occlusion是采样AO贴图获得的,Unity_GlossyEnvironmentData是刚计算的roughness和reflUVW
函数内使用了两个方法,首先是第一个方法UnityGI_Base,我们来看看它的本体(大波骚操作警告):
inline UnityGI UnityGI_Base(UnityGIInput data, half occlusion, half3 normalWorld)//UnityGI_Base链接
{
    UnityGI o_gi;// 声明返回变量
   
    ResetUnityGI(o_gi);// 重置GI变量,其中别的坐标全部重置为0,唯独线性光方向重置为(0,1,0),避免0向量。
   
    // Base pass with Lightmap support is responsible for handling ShadowMask / blending here for performance reason
    // 处理distanceShadowMask从realtimeShadow到shadowmask的混合。需要开启质量设置内的distanceShadowMask
    #if defined(HANDLE_SHADOWS_BLENDING_IN_GI)half bakedAtten = UnitySampleBakedOcclusion(data.lightmapUV.xy, data.worldPos);
        float zDist = dot(_WorldSpaceCameraPos - data.worldPos, UNITY_MATRIX_V[2].xyz);
        float fadeDist = UnityComputeShadowFadeDistance(data.worldPos, zDist);
        data.atten = UnityMixRealtimeAndBakedShadows(data.atten, bakedAtten, UnityComputeShadowFade(fadeDist));
    #endif
   
    //必将执行的两句。将直接光照赋予给最终的gi。
    o_gi.light = data.light;
    o_gi.light.color *= data.atten;//处理衰减
   
   
    #if UNITY_SHOULD_SAMPLE_SH //球谐光照计算时,间接光直接赋予球谐计算方法。
        o_gi.indirect.diffuse = ShadeSHPerPixel(normalWorld, data.ambient, data.worldPos);
    #endif
   
    #if defined(LIGHTMAP_ON)
        // Baked lightmaps使用烘焙光照时
        half4 bakedColorTex = UNITY_SAMPLE_TEX2D(unity_Lightmap, data.lightmapUV.xy);
        half3 bakedColor = DecodeLightmap(bakedColorTex);//因为光照贴图是HDR贴图(超过0,1范围),所以要Decode。
        // 处理好后先放着,等下加
        #ifdef DIRLIGHTMAP_COMBINED//假如有烘焙方向贴图的话
            fixed4 bakedDirTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_LightmapInd, unity_Lightmap, data.lightmapUV.xy);
            o_gi.indirect.diffuse += DecodeDirectionalLightmap(bakedColor, bakedDirTex, normalWorld);
            //对方向光的采样和解码,然后加到之前烘焙光照算出的diffuse
            #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN)
                ResetUnityLight(o_gi.light);
                o_gi.indirect.diffuse = SubtractMainLightWithRealtimeAttenuationFromLightmap(o_gi.indirect.diffuse, data.atten, bakedColorTex, normalWorld);
                // 从灯光贴图中减掉带衰减的主灯。
            #endif
            
        #else // not directional lightmap若果没有方向贴图,diffuse直接添加LightMap采样,
            o_gi.indirect.diffuse += bakedColor;
            #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN)
                ResetUnityLight(o_gi.light);
                o_gi.indirect.diffuse = SubtractMainLightWithRealtimeAttenuationFromLightmap(o_gi.indirect.diffuse, data.atten, bakedColorTex, normalWorld);
                // 同样需要减掉主灯
            #endif
        #endif
    #endif
   
    #ifdef DYNAMICLIGHTMAP_ON
        // Dynamic lightmaps//动态烘焙采样
        fixed4 realtimeColorTex = UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, data.lightmapUV.zw);
        half3 realtimeColor = DecodeRealtimeLightmap(realtimeColorTex);
        
        #ifdef DIRLIGHTMAP_COMBINED
            //如果有计算方向贴图,方向贴图采样和解码
            half4 realtimeDirTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_DynamicDirectionality, unity_DynamicLightmap, data.lightmapUV.zw);
            o_gi.indirect.diffuse += DecodeDirectionalLightmap(realtimeColor, realtimeDirTex, normalWorld);
        #else
            o_gi.indirect.diffuse += realtimeColor;
            //注意这里加了烘焙GI和实时GI,有两个相加项。证明两者在计算时不会重复计算。
        #endif
    #endif
   
    o_gi.indirect.diffuse *= occlusion;//最后乘上AO
   
    return o_gi;
}这个方法这么大的原因,是因为自带了烘焙灯光和阴影的各种分支。这个部分比较多内容,但我还是发上来可以让读者看一下我自己做的一些注释。
假如把干扰项都排除掉,去掉烘焙部分后,它的代码是:
inline UnityGI UnityGI_Base(UnityGIInput data, half occlusion, half3 normalWorld)//UnityGI_Base链接
{
    UnityGI o_gi;// 声明返回变量
   
    ResetUnityGI(o_gi);// 重置GI变量,其中别的坐标全部重置为0,唯独线性光方向重置为(0,1,0),避免0向量。
   
    //必将执行的两句。将直接光照赋予给最终的gi。
    o_gi.light = data.light;
    o_gi.light.color *= data.atten;//处理衰减
      
    #if UNITY_SHOULD_SAMPLE_SH //球谐光照计算时,间接光直接赋予球谐计算方法。
        o_gi.indirect.diffuse = ShadeSHPerPixel(normalWorld, data.ambient, data.worldPos);
    #endif

    o_gi.indirect.diffuse *= occlusion;//最后乘上AO
   
    return o_gi;
}瞬间清爽了不少,我们来看看这里面做了什么
    声明了返回变量 返回变量归零,这里的ResetUnityGI是一个专门用于重置UnityGI类型的方法,和一般的归零不同在于,DirectionalLight的位置其实是方向矢量,那么这个矢量全归零,就不是方向了,单独为这种类型的灯光位置设置为(0,1,0)避免这个问题。 将直接灯光信息注入返回值,直接光照强度作衰减处理(衰减系数在前面已经传入) 假如有光照探针或者球谐计算,使用ShadeSHPerPixel函数进行球谐计算,这里的算法有比较复杂的原理支撑,这个部分在骥云的这篇文章中有比较好的解释,在网页中搜索ShadeSHPerPixel就可以找到这部分的讲解。(这篇文章也很好的讲了standardShader的算法架构,很值得细细品读)

    按我自己的理解,这个部分其实也属于IBL的漫反射部分,之后的文章我会提到,实际的PBR的构成是直接光和IBL两个部分,然后IBL又分为漫反射和高光反射部分。那么IBL的漫反射部分在最终的BRDF之前已经在这里就算出来了。 最后把indirect.diffuse乘上occlusion就可以得到最终的间接光漫反射值。
那么总结一下,说了这么多,UnityGI_Base其实主要就是计算间接光的漫反射部分(也包括IBL的漫反射部分),然后捎带把衰减系数添加入了直接光照的强度部分。
2.UnityGlobalIllumination函数及其中的UnityGI_IndirectSpecular函数(计算间接光照Specular部分(BRDF前))

接下来再看UnityGlobalIllumination函数中的UnityGI_IndirectSpecular函数。
我们先拿本体看看(大波骚操作警告):
//Specular部分计算 //伪代码
inline half3 UnityGI_IndirectSpecular(UnityGIInput data, half occlusion, Unity_GlossyEnvironmentData glossIn)
{
    //声明返回变量。
    half3 specular;
    #ifdef UNITY_SPECCUBE_BOX_PROJECTION//开启反射探针的boxprojection功能(此功能不将反射环境看做无限大,物体看做无限小。根据采样点与反射探针box的相对位置采样反射)
        // 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//保留glossIn.reflUVW变量,新建一个变量。
        half3 originalReflUVW = glossIn.reflUVW;
        glossIn.reflUVW = BoxProjectedCubemapDirection (originalReflUVW, data.worldPos, data.probePosition[0], data.boxMin[0], data.boxMax[0]);//BoxProject反射方法
    #endif
   
    //如果勾选了材质面板禁用反射功能的话(unity默认standard材质选项。用c#写的,在材质面板最靠下的位置)
    #ifdef _GLOSSYREFLECTIONS_OFF
        specular = unity_IndirectSpecColor.rgb;
    #else
        //Specular重点是这条语句,整个方法就是把这个语句算出的值赋给specular变量然后返回。
        half3 env0 = Unity_GlossyEnvironment (UNITY_PASS_TEXCUBE(unity_SpecCube0), data.probeHDR[0], glossIn);
        //UNITY_PASS_TEXCUBE(unity_SpecCube0)根据不同条件传递反射Cubemap函数。data.probeHDR[0]:反射探针采样后解码用参数(Unity给出)。glossIn:粗糙度和反射采样坐标。
        
        #ifdef UNITY_SPECCUBE_BLENDING//开启多个反射探针的混合
            const float kBlendFactor = 0.99999;
            float blendLerp = data.boxMin[0].w;
            UNITY_BRANCH
            if (blendLerp < kBlendFactor)
            {
                #ifdef UNITY_SPECCUBE_BOX_PROJECTION
                    glossIn.reflUVW = BoxProjectedCubemapDirection (originalReflUVW, data.worldPos, data.probePosition[1], data.boxMin[1], data.boxMax[1]);
                #endif
                half3 env1 = Unity_GlossyEnvironment (UNITY_PASS_TEXCUBE_SAMPLER(unity_SpecCube1,unity_SpecCube0), data.probeHDR[1], glossIn);
                specular = lerp(env1, env0, blendLerp);
            }
            else
            {
                specular = env0;
            }
        #else
            specular = env0;
        #endif
    #endif
    return specular * occlusion;//diffuse和specular计算完之后都要乘以AO
}那么这里面涉及3个分支或子分支:
    Box_Projection分支 正常必走分支 SpecCube_Blend分支
1>Box_Projection和SpecCube_Blend分支

我们先看看什么是Box_Projection


之前我们用卡姿兰大眼睛讲解了一般情况下的反射采样方法,这个方法的是将天空盒看成无限大的环境,然后无论物体的位置如何变化,只要法线和入射方向相同,采样方向都是相同的。
而boxProjection功能会考虑物体和反射探针范围的相对位置,此时反射探针是有范围的,如下图


这个时候不同物体在不同位置的采样点是不同的,也就是说,反射效果和物体的位置有关系。
那具体到游戏画面中,我们可以看到这样的效果:



开启BoxProjection



关闭BoxProjection

可以看到,开启BoxProjection后,反射效果互动感更加真实。
这个BoxProjection的开关在反射探针的设置选项这里:


最后还有一个CubeMap_Blend分支
这个Blend部分在我上一篇文章靠近末尾处有介绍,具体可以点击链接查看:
2>UnityGI_IndirectSpecular函数必走分支

假如把刚才的BoxProjection和CubeMap_Blend分支去除,那么我们的UnityGI_IndirectSpecular函数就清爽可爱了,请看本体
//Specular部分计算
inline half3 UnityGI_IndirectSpecular(UnityGIInput data, half occlusion, Unity_GlossyEnvironmentData glossIn)
{
    //声明返回变量。
    half3 specular;
   
    //如果勾选了材质面板禁用反射功能的话(unity默认standard材质选项。用c#写的,在材质面板最靠下的位置)
    #ifdef _GLOSSYREFLECTIONS_OFF
        specular = unity_IndirectSpecColor.rgb;
    #else
        //Specular重点是这条语句,整个方法就是把这个语句算出的值赋给specular变量然后返回。
        half3 env0 = Unity_GlossyEnvironment (UNITY_PASS_TEXCUBE(unity_SpecCube0), data.probeHDR[0], glossIn);
        //UNITY_PASS_TEXCUBE(unity_SpecCube0)根据不同条件传递反射Cubemap函数。data.probeHDR[0]:反射探针采样后解码用参数(Unity给出)。glossIn:粗糙度和反射采样坐标。
         specular = env0;

    #endif
    return specular * occlusion;//diffuse和specular计算完之后都要乘以AO
}这里面走了这么几步:
    声明返回变量 假如勾选禁用反射功能,就直接返回内置的unity_IndirectSpecColor.rgb(这个值测试来测试去也不知道是什么,网上也没有,猜测是反射探针或者天空盒的平均颜色,有知道的同学可以补充一下) 禁用反射功能就是在材质球面板关闭此选项


以上分支判断完后,假如使用反射功能,则走Unity_GlossyEnvironment。
Unity_GlossyEnvironment函数的主要功能,是使用一个由roughness转换而来的mip系数,对反射贴图cubmap的mipmap进行采样。


这个mipmap有很多个层级,Unity默认采样0-6级。当粗糙度越高时,采样的级别越高。这就是roughness效果的视觉原理。
mipmap实际上就是一个贴图的LOD技术,在将一张贴图转换为cubemap的时候,Unity就会自动计算如下的mipmap阵列,以待之后的采样所需。



此图片来自Foogywoo

了解完原理之后,我们来看看这个Unity_GlossyEnvironment函数的本体:
//这个方法来自于UnityImageBasedLighting.cginc函数。所有有关HDRI反射的渲染的方法。
//UNITY_PASS_TEXCUBE(unity_SpecCube0)采样反射探针CubeMap的默认函数。hdr:反射探针采样后解码用参数(在shader里通过giInput.data.probeHDR[0]赋予)。glossIn:粗糙度和反射采样坐标。
half3 Unity_GlossyEnvironment (UNITY_ARGS_TEXCUBE(tex), half4 hdr, Unity_GlossyEnvironmentData glossIn)
{
    half perceptualRoughness = glossIn.roughness /* perceptualRoughness */ ;//输入粗糙度(非得要说是感性粗糙度。。。)
   
    //灰色注释掉部分,是还没有写完的待完成的部分,用#if 0 强制注释掉了。
    // TODO: CAUTION: remap from Morten may work only with offline convolution, see impact with runtime convolution!
    // For now disabled
    #if 0
        float m = PerceptualRoughnessToRoughness(perceptualRoughness); // m is the real roughness parameter
        const float fEps = 1.192092896e-07F;        // smallest such that 1.0+FLT_EPSILON != 1.0  (+1e-4h is NOT good here. is visibly very wrong)
        float n =  (2.0/max(fEps, m*m))-2.0;        // remap to spec power. See eq. 21 in --> https://dl.dropboxusercontent.com/u/55891920/papers/mm_brdf.pdf
        n /= 4;                                     // remap from n_dot_h formulatino to n_dot_r. See section "Pre-convolved Cube Maps vs Path Tracers" --> https://s3.amazonaws.com/docs.knaldtech.com/knald/1.0.0/lys_power_drops.html
        perceptualRoughness = pow( 2/(n+2), 0.25);      // remap back to square root of real roughness (0.25 include both the sqrt root of the conversion and sqrt for going from roughness to perceptualRoughness)
    #else
        // MM: came up with a surprisingly close approximation to what the #if 0'ed out code above does.
        //rough = rough*(1.7-0.7*rough)转换粗糙度为Mipmap的采样近似值。粗糙度原理:越粗糙,采样的反射cubemap的mipmap分辨率就越小。越光滑,采样的反射cubemap的mipmap分辨率就越大。
        //由于粗糙度与反射探针的mip变化不呈线性正比,所以需要一个公式来改变。
        perceptualRoughness = perceptualRoughness*(1.7 - 0.7*perceptualRoughness);
    #endif
   
    half mip = perceptualRoughnessToMipmapLevel(perceptualRoughness);//return perceptualRoughness * UNITY_SPECCUBE_LOD_STEPS(返回值范围0-6);默认UNITY_SPECCUBE_LOD_STEPS == 6意思是环境反射默认是6级mipmap;
    half3 R = glossIn.reflUVW;
    half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(tex, R, mip);//根据mip值对反射cubemap进行LOD采样的方法。
    return DecodeHDR(rgbm, hdr);//对采样值进行HDR解码
}里面有一个很长的部分,#if 0,这是一段待检测的代码,其实是一个精确但是计算量比较大的算法。这是暂时还没有使用的部分,我们先删去他,那么剩下的是一段清爽的代码
//UNITY_PASS_TEXCUBE(unity_SpecCube0)采样反射探针CubeMap的默认函数。hdr:反射探针采样后解码用参数(在shader里通过giInput.data.probeHDR[0]赋予)。glossIn:粗糙度和反射采样坐标。
half3 Unity_GlossyEnvironment (UNITY_ARGS_TEXCUBE(tex), half4 hdr, Unity_GlossyEnvironmentData glossIn)
{
    half perceptualRoughness = glossIn.roughness /* perceptualRoughness */ ;//输入粗糙度(非得要说是感性粗糙度。。。)
    //由于粗糙度与反射探针的mip变化不呈线性正比,所以需要一个公式来改变。
    perceptualRoughness = perceptualRoughness*(1.7 - 0.7*perceptualRoughness);
    //return perceptualRoughness * UNITY_SPECCUBE_LOD_STEPS(返回值范围0-6);默认UNITY_SPECCUBE_LOD_STEPS == 6意思是环境反射默认是6级mipmap;
    half mip = perceptualRoughnessToMipmapLevel(perceptualRoughness);
    half3 R = glossIn.reflUVW;
    half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(tex, R, mip);//根据mip值对反射cubemap进行LOD采样的方法。
    return DecodeHDR(rgbm, hdr);//对采样值进行HDR解码
}这个函数的输入值
    UNITY_ARGS_TEXCUBE(tex) 输入的是UNITY_PASS_TEXCUBE(unity_SpecCube0)这个里面采样了默认的反射探针贴图,然后返回了一个HDR贴图。 half4 hdr传入的是data.probeHDR[1],这个里面是在数据准备阶段准备的反射HDR解码数据。 Unity_GlossyEnvironmentData glossIn里面传入的是粗糙度和反射坐标
这个函数最终返回的是一个half3,也就是对反射的完整采样颜色。
进入函数后,首先获得Roughness值
然后使用一个perceptualRoughness*(1.7-0.7*perceptualRoughness);对roughness进行变换。这个变换函数长这个样子:


也就是roughness进行粗糙度采样mipmap时,并不是线性的,用这个函数进行合理的变化。
接下来再使用perceptualRoughnessToMipmapLevel(perceptualRoughness);其实内部就是讲roughness从0-1映射到0-6。因为最终默认要采样的模糊的cubemap有0-6个层级。
然后传入反射采样坐标glossIn.reflUVW。
再将所有的数据  传入的HDR贴图,反射采样坐标,由roughness变换得到的mip采样系数   这三个数据来采样反射CubeMap。使用 UNITY_SAMPLE_TEXCUBE_LOD(tex, R, mip)函数。这个就是我们之前说的不同粗糙度采样不同的mipmap的主要函数。
注意这个函数里面不是阶梯状的采样,而是多张相邻mipmap线性插值的过程,所以不同的roughness对应到的采样会是线性过度的,不会突变。
采样完Cubemap后,我们获得一个HDR颜色,需要使用之前准备的反射解码参数data.probeHDR[1],并使用DecodeHDR(rgbm, hdr)进行解码,获得可以返回的RGB颜色。
也就是我们苦苦追寻的基于IBL的SpecColor(BRDF前)
3>最终高光反射数据的返回

我们的数据经过了长途跋涉,和层层的分支过滤之后,终于得到了基于IBL的specColor。我想读者假如没有一步步自己操作应该都已经绕晕了。现在我们看看它是怎么一层层回到最终的返回值的。
    首先通过Unity_GlossyEnvironment函数返回给env0变量。 在UnityGI_IndirectSpecular中,env0和AO相乘,返回给o_gi.indirect.specular。 indirect.specular在UnityGlobalIllumination里和计算好的indirect.diffuse和gi.light一起以UnityGI gi的形式返回给LightingStandard_GI里的gi变量,再通过函数inout变量的形式将完整的全局光照UnityGI gi传出。
值得注意的是,这里面计算的颜色和变量,都没有经过BRDF计算。缺少PBR物理特性所带来的各种衰减和效果变化,以及能量守恒的关系。这些,都将在本系列的下一篇文章中给出。
这里我们再回顾一下这个函数的分支图:


至此,全局光照函数计算完毕。
总结:

本文为系列文章,完整文章链接在此:
此篇第三篇文章大部分地方都在讲代码,为了让更广泛的读者可以阅读,我说的比较啰嗦。但是我研究完整个流程之后,发现只有这样才能完整的理顺这部分的内容。希望读者和关注我的人有所收获。下篇文章BRDF部分是此次系列文章的重点,也是高潮,敬请关注!
在写作这篇文章时,我只是一个刚接触shader内容的模型师,文内可能有所疏漏,希望能有大佬斧正。我参考了如下资料,献出我的膝盖以示感谢:
    taecg老师的系列教学《渲染管线与UnityShader编程》 冯乐乐女神的书《UnityShader入门精要》 毛星云(浅墨)的系列文章《基于物理的渲染(PBR)白皮书》
peace:)

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2021-11-20 20:34 | 显示全部楼层
很强
发表于 2021-11-20 20:38 | 显示全部楼层
厉害,另外那个眼睛画得很好
发表于 2021-11-20 20:46 | 显示全部楼层
有点用,但实例和分析感觉都比较少
发表于 2021-11-20 20:48 | 显示全部楼层
主要是了解官方写法的思路,为了能改写和利用。
发表于 2021-11-20 20:53 | 显示全部楼层
请问下,我照着这个结构体撸了一遍,但我的shader 无法受到天光影响,这是什么原因呢?cginclude都有
发表于 2021-11-20 20:54 | 显示全部楼层
太妙了~
发表于 2021-11-20 20:54 | 显示全部楼层
超级干货 帮大忙
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-12-22 20:25 , Processed in 0.099112 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表