|
这篇文章是系列文章,具体讲了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 &#34;Pre-convolved Cube Maps vs Path Tracers&#34; --> 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&#39;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:)
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|