找回密码
 立即注册
查看: 818|回复: 15

[笔记] 【学习笔记】Unity PBR的实现

[复制链接]
发表于 2022-1-4 10:12 | 显示全部楼层 |阅读模式
前言

哈哈,终于看完了Standard的源码了(emmm一部分),再结合之前学习PBR的知识,现在感觉浑身充满了能量,便决定自己实现一个PBR的shader,来熟悉一下,若有什么不对还请大佬指教...
感觉和大佬们差了好几个次元,究竟什么时候能够和大佬一起看太阳...
关于PBR的知识网上已经有很多了,所以我就不再这说了....如果不太清楚的话,可以看看下面的几个文章
《UnityShader入门精要》的第18章基于物理渲染
浅墨大大的游戏编程
猴子都能看懂的PBR(才怪)
还有很多大佬优秀的文章,这里就不一一列举了
使用的公式

这里会写出这个PBR使用的公式,你可能会发现某些东西好像都来自同一个地方==【哭.gif】
首先最重要的就是我们的渲染方程



来自《unityShader入门精要》

该公式描述了,一个点的亮度是受许多不同方向的入射光Li(wi)不同影响的累加结果,累加就是对该点进行求半球积分,但是积分累加部分在实时渲染基本无法实现,因此积分部分通常会被替换成若干精确光源的叠加



来自《unityShader入门精要》

替换后就是这个样子了...看起来简洁了不少。这描述了该点受一个精确光照影响后的颜色,f(lc,v)是双向反射分布函数(BRDF),Clight为精确光源的颜色,n·lc为该光的投影结果(也就是有多少光会作用到该点上)。
双向反射分布函数BRDF

他描述了光在该点是如何分布的,也就是说当一个光照向一个物体时,光是如何反射到我们眼睛的。
光在照向物体时会有一部分被镜面反射,有一部分会被漫反射



来自《unityShader入门精要》

如上图所示,褐色为光的入射方向,橘黄色为镜面反射,蓝色为漫反射。在左图中,我们可以看到当像素大小小于漫反射的散射距离时,这些散射光会从别的像素点射出,这被称为次表面散射。在右图中,当像素大小大于漫反射的散射距离时,这些散射我们可以看做都是从这个像素点射出去的,而BRDF就是用来描述他的,我们下面的计算也都是基于后者的。
所以BRDF将会有漫反射项和镜面反射项两项
漫反射项使用的是Disney的



来自《unityShader入门精要》

镜面反射项为



来自《unityShader入门精要》

F为菲涅尔反射,他描述了会有多少光来参与镜面反射,就像我们前面所说的只有一部分光会被镜面反射,菲涅尔项就是来计算这个比率的



来自《unityShader入门精要》

D为法线分布函数,他描述了有多少光会反射到观察方向上,因为是镜面反射,所以我们只考虑那些能够直接反射到我们眼睛的光,使用的是基于GGX的公式



来自《unityShader入门精要》

G为阴影遮掩函数,他描述了有多少光不会被遮挡,有些光即使满足了上面的所有条件,但还有可能会被遮挡,所以我们也必须将这些会被遮挡的光也去除掉,一般会和镜面反射的分母(n·l)(n·v)进行结合,称为可见性项,这次使用的公式是Smith-Joint



将入门精要上的推导给省略了==

以上就是这次PBR使用的公式,也是unity使用的一些公式,这方面还是非常建议看看大佬的文章...
思路

虽然说是思路,其实也是顺着standard shader写的,所以你可能会发现最后实现的效果和standard没什么多大的区别
先来整理一下我们需要计算的光源有哪些:直接光照,间接光照,自发光,最后的光照效果也是计算完这3个光,加起来就可以了。
直接光照,就是直接受到光源影响的光照,使用上面的渲染方程就可以算出
间接光照,物体除了会受到直接光源的影响,还会受到周围物体所反射的光和环境光,这也分为间接漫反射和镜面反射。对于漫反射,如果物体是静态的,unity会给我们烘焙到lightmap上,我们对其采样就可以,而动态物体,则是采样光照探头。对于镜面反射,我们会采样反射探头来描绘物体所反射的内容,这也被称为IBL部分
自发光,就是物体自己发出的光,直接加就可以。一般自发光还会对别的物体产生影响,这需要写一个额外的pass,这里并没有写==
1.一开始的话我们当然是进行数据准备(有了数据我们才能对那些公式进行计算),包含了对材质属性的声明、进行必要的坐标转换、计算采样纹理坐标、采样纹理、计算会使用参数的值等,顶点着色器和片元着色器的前半部分进行了这样的工作
2.光照向物体时一部分会被镜面反射,一部分会被漫反射。为了能量守恒,我们需要计算出镜面反射所占的比率也就是specColor,以及漫反射所占的比率也就是diffColor
3.我们先计算了间接光照,对于间接光漫反射和镜面反射在上面已经说了...
4.接下来就是计算BRDF,我们先计算了BRDF的镜面反射项,然后计算了漫反射项
5.输出颜色,将BRDF带入渲染方程在加上间接光照和自发光,最为最后的颜色,再添加上雾效的影响输出。
实现

这个PBR是金属工作流的,下面是我们需要的一些材质属性
_Color("Color",color) = (1,1,1,1)        //颜色
_MainTex("Albedo",2D) = "white"{}        //反照率
_MetallicGlossMap("Metallic",2D) = "white"{} //金属图,r通道存储金属度,a通道存储光滑度
_BumpMap("Normal Map",2D) = "bump"{}//法线贴图
_OcclusionMap("Occlusion",2D) = "white"{}//环境光遮挡纹理
_MetallicStrength("MetallicStrength",Range(0,1)) = 1 //金属强度
_GlossStrength("Smoothness",Range(0,1)) = 0.5 //光滑强度
_BumpScale("Normal Scale",float) = 1 //法线影响大小
_EmissionColor("Color",color) = (0,0,0) //自发光颜色
_EmissionMap("Emission Map",2D) = "white"{}//自发光贴图接下来是我们顶点函数的输出和输入结构体
struct a2v
{
        float4 vertex : POSITION;
        float3 normal : NORMAL;
        float4 tangent :TANGENT;
        float2 texcoord : TEXCOORD0;
        float2 texcoord1 : TEXCOORD1;
        float2 texcoord2 : TEXCOORD2;
};
struct v2f
{
        float4 pos : SV_POSITION;
        float2 uv : TEXCOORD0;
        half4 ambientOrLightmapUV : TEXCOORD1;//存储环境光或光照贴图的UV坐标
        float4 TtoW0 : TEXCOORD2;
        float4 TtoW1 : TEXCOORD3;
        float4 TtoW2 : TEXCOORD4;//xyz 存储着 从切线空间到世界空间的矩阵,w存储着世界坐标
        SHADOW_COORDS(5) //定义阴影所需要的变量,定义在AutoLight.cginc
        UNITY_FOG_COORDS(6) //定义雾效所需要的变量,定义在UnityCG.cginc
};在顶点输入结构体中,我们声明的texcoord1和texcoord2是为了我们之后计算动态和静态光照贴图的uv坐标。
在顶点函数就比较简单了,主要是计算了片元函数所需要的一些数据
v2f vert(a2v v)
{
        v2f o;
        UNITY_INITIALIZE_OUTPUT(v2f,o);//初始化结构体数据,定义在HLSLSupport.cginc

        o.pos = UnityObjectToClipPos(v.vertex);//将模型空间转换到裁剪空间,定义在UnityShaderUtilities.cginc
        o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);//计算偏移后的uv坐标,定义在UnityCG.cginc

        float3 worldPos = mul(unity_ObjectToWorld,v.vertex);
        half3 worldNormal = UnityObjectToWorldNormal(v.normal);
        half3 worldTangent = UnityObjectToWorldDir(v.tangent);
        half3 worldBinormal = cross(worldNormal,worldTangent) * v.tangent.w;

        //计算环境光照或光照贴图uv坐标
        o.ambientOrLightmapUV = VertexGI(v.texcoord1,v.texcoord2,worldPos,worldNormal);

        //前3x3存储着从切线空间到世界空间的矩阵,后3x1存储着世界坐标
        o.TtoW0 = float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
        o.TtoW1 = float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);
        o.TtoW2 = float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);

        //填充阴影所需要的参数,定义在AutoLight.cginc
        TRANSFER_SHADOW(o);
        //填充雾效所需要的参数,定义在UnityCG.cginc
        UNITY_TRANSFER_FOG(o,o.pos);

        return o;
}这里需要说明的就是VertexGI这个函数了,我们在计算间接光漫反射时,我们判断物体是否是动态物体,如果是动态物体,则计算非重要的光源(包含顶点光照和球谐光照,光照探头存储的光照数据在球谐光照中),然而对于静态物体,则是对其进行计算采样静态和动态光照贴图的采样坐标,然后在片元函数中进行采样
//计算环境光照或光照贴图uv坐标
inline half4 VertexGI(float2 uv1,float2 uv2,float3 worldPos,float3 worldNormal)
{
        half4 ambientOrLightmapUV = 0;

        //如果开启光照贴图,计算光照贴图的uv坐标
        #ifdef LIGHTMAP_ON
                ambientOrLightmapUV.xy = uv1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
                //仅对动态物体采样光照探头,定义在UnityCG.cginc
        #elif UNITY_SHOULD_SAMPLE_SH
                //计算非重要的顶点光照
                #ifdef VERTEXLIGHT_ON
                        //计算4个顶点光照,定义在UnityCG.cginc
                        ambientOrLightmapUV.rgb = Shade4PointLights(
                                unity_4LightPosX0,unity_4LightPosY0,unity_4LightPosZ0,
                                unity_LightColor[0].rgb,unity_LightColor[1].rgb,unity_LightColor[2].rgb,unity_LightColor[3].rgb,
                                unity_4LightAtten0,worldPos,worldNormal);
                #endif
                //计算球谐光照,定义在UnityCG.cginc
                ambientOrLightmapUV.rgb += ShadeSH9(half4(worldNormal,1));
        #endif

        //如果开启了 动态光照贴图,计算动态光照贴图的uv坐标
        #ifdef DYNAMICLIGHTMAP_ON
                ambientOrLightmapUV.zw = uv2.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
        #endif

        return ambientOrLightmapUV;
}这个函数也可以在UnityStandardCore.cginc中找到,是VertexGIForward函数,不同的是unity对其进行了更多的处理,比如对不同平台的优化
接下来,在片元函数中,先将之前声明的材质属性进行处理,转换成我们能够直接使用的变量,比如对之前声明的纹理进行采样,计算灯光、观察、反射方向,以及BRDF需要用到的一些数量积等
//数据准备
float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w);//世界坐标
half3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb;//反照率
half2 metallicGloss = tex2D(_MetallicGlossMap,i.uv).ra;
half metallic = metallicGloss.x * _MetallicStrength;//金属度
half roughness = 1 - metallicGloss.y * _GlossStrength;//粗糙度
half occlusion = tex2D(_OcclusionMap,i.uv).g;//环境光遮挡

//计算世界空间中的法线
half3 normalTangent = UnpackNormal(tex2D(_BumpMap,i.uv));
normalTangent.xy *= _BumpScale;
normalTangent.z = sqrt(1.0 - saturate(dot(normalTangent.xy,normalTangent.xy)));
half3 worldNormal = normalize(half3(dot(i.TtoW0.xyz,normalTangent),
                                        dot(i.TtoW1.xyz,normalTangent),dot(i.TtoW2.xyz,normalTangent)));

half3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));//世界空间下的灯光方向,定义在UnityCG.cginc
half3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));//世界空间下的观察方向,定义在UnityCG.cginc
half3 refDir = reflect(-viewDir,worldNormal);//世界空间下的反射方向

half3 emission = tex2D(_EmissionMap,i.uv).rgb * _EmissionColor;//自发光颜色

UNITY_LIGHT_ATTENUATION(atten,i,worldPos);//计算阴影和衰减,定义在AutoLight.cginc

//计算BRDF需要用到一些项
half3 halfDir = normalize(lightDir + viewDir);
half nv = saturate(dot(worldNormal,viewDir));
half nl = saturate(dot(worldNormal,lightDir));
half nh = saturate(dot(worldNormal,halfDir));
half lv = saturate(dot(lightDir,viewDir));
half lh = saturate(dot(lightDir,halfDir));然后我们需要计算BRDF需要用到的镜面反射率,漫反射率。这描述这有多少光参加镜面反射或漫反射
//计算镜面反射率
half3 specColor = lerp(unity_ColorSpaceDielectricSpec.rgb,albedo,metallic);
//计算1 - 反射率,漫反射总比率
half oneMinusReflectivity = (1- metallic) * unity_ColorSpaceDielectricSpec.a;
//计算漫反射率
half3 diffColor = albedo * oneMinusReflectivity;unity_ColorSpaceDielectricSpec.rgb定义着物体的基础镜面反射率,我们通过metallic来决定镜面反射率,unity_ColorSpaceDielectricSpec.a存储的是1-dielectricSpec,  oneMinusReflectivity计算公式推导可以从UnityStandardUtils.cginc中 OneMinusReflectivityFromMetallic函数中找到,然而紧跟着的就是Unity计算金属流镜面反射率和漫反射率的函数,oneMinusReflectivity还会在后面计算掠射颜色时会使用到
接着我们计算间接光照,间接光照包含间接漫反射和间接镜面反射
//计算间接光
half3 indirectDiffuse = ComputeIndirectDiffuse(i.ambientOrLightmapUV,occlusion);//计算间接光漫反射
half3 indirectSpecular = ComputeIndirectSpecular(refDir,worldPos,roughness,occlusion);//计算间接光镜面反射计算间接漫反射时,我们判断物体是否是动态物体,如果是动态物体,漫反射就是之前在顶点函数计算的非重要光源,然而对于静态物体,则是对静态和动态光照贴图进行采样的结果,最后在乘上遮罩对其的影响
//计算间接光漫反射
inline half3 ComputeIndirectDiffuse(half4 ambientOrLightmapUV,half occlusion)
{
        half3 indirectDiffuse = 0;

        //如果是动态物体,间接光漫反射为在顶点函数中计算的非重要光源
        #if UNITY_SHOULD_SAMPLE_SH
                indirectDiffuse = ambientOrLightmapUV.rgb;       
        #endif

        //对于静态物体,则采样光照贴图或动态光照贴图
        #ifdef LIGHTMAP_ON
                //对光照贴图进行采样和解码
                //UNITY_SAMPLE_TEX2D定义在HLSLSupport.cginc
                //DecodeLightmap定义在UnityCG.cginc
                indirectDiffuse = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap,ambientOrLightmapUV.xy));
        #endif
        #ifdef DYNAMICLIGHTMAP_ON
                //对动态光照贴图进行采样和解码
                //DecodeRealtimeLightmap定义在UnityCG.cginc
                indirectDiffuse += DecodeRealtimeLightmap(UNITY_SAMPLE_TEX2D(unity_DynamicLightmap,ambientOrLightmapUV.zw));
        #endif

        //将间接光漫反射乘以环境光遮罩,返回
        return indirectDiffuse * occlusion;
}在这里我们省去了DIRLIGHTMAP对其的影响,也使得我们的代码简单了不少,如果感兴趣的话可以去UnityGlobalIllumination.cginc中UnityGI_Base函数中找到相关的实现,我理解是将采样得到的光照数据,根据DIRLIGHTMAP中的方向和worldNormal进行重新计算兰伯特光照
在计算间接镜面反射时,会比较麻烦一些,我们在这里处理了两个反射探头,首先对第一个反射探头反射方向进行重新映射和采样,然后判断是否使用了第二个反射探头,如果使用了第二个反射探头,则对其进行采样并混合,最后乘上遮罩对其的影响
//计算间接光镜面反射
inline half3 ComputeIndirectSpecular(half3 refDir,float3 worldPos,half roughness,half occlusion)
{
        half3 specular = 0;
        //重新映射第一个反射探头的采样方向
        half3 refDir1 = BoxProjectedDirection(refDir,worldPos,unity_SpecCube0_ProbePosition,unity_SpecCube0_BoxMin,unity_SpecCube0_BoxMax);
        //对第一个反射探头进行采样
        half3 ref1 = SamplerReflectProbe(UNITY_PASS_TEXCUBE(unity_SpecCube0),refDir1,roughness,unity_SpecCube0_HDR);
        //如果第一个反射探头的权重小于1的话,我们将会采样第二个反射探头,进行混合
        //使下面的if语句产生分支,定义在HLSLSupport.cginc中
        UNITY_BRANCH
        if(unity_SpecCube0_BoxMin.w < 0.99999)
        {
                //重新映射第二个反射探头的方向
                half3 refDir2 = BoxProjectedDirection(refDir,worldPos,unity_SpecCube1_ProbePosition,unity_SpecCube1_BoxMin,unity_SpecCube1_BoxMax);
                //对第二个反射探头进行采样
                half3 ref2 = SamplerReflectProbe(UNITY_PASS_TEXCUBE_SAMPLER(unity_SpecCube1,unity_SpecCube0),refDir2,roughness,unity_SpecCube1_HDR);

                //进行混合
                specular = lerp(ref2,ref1,unity_SpecCube0_BoxMin.w);
        }
        else
        {
                specular = ref1;
        }
        return specular * occlusion;
}对于反射方向的重新映射和反射探头的采样将在后面说...
UNITY_BRANCH他会让下面的if语句产生一个分支,而与其对立的是UNITY_FLATTEN,他始终会运行if的两个结果的所有语句,并在完成后选择一个正确的结果。他们定义在HLSLSupport.cginc中,他们其实就是HLSL平台上的[branch]和[flatten],在其他平台上什么事都不做。关于这个更多的信息可以看这里
https://forum.unity.com/threads/correct-use-of-unity_branch.476804/
#if defined(UNITY_COMPILER_HLSL)
        #define UNITY_BRANCH        [branch]
        #define UNITY_FLATTEN        [flatten]
        #define UNITY_UNROLL        [unroll]
        #define UNITY_LOOP        [loop]
        #define UNITY_FASTOPT        [fastopt]
#else
        #define UNITY_BRANCH
        #define UNITY_FLATTEN
        #define UNITY_UNROLL
        #define UNITY_LOOP
        #define UNITY_FASTOPT
#endif继续上面的说unity_SpecCube0_BoxMin.w存储着第一个反射探头的权重,当小于1时我们将会混合第二个反射探头的数据
接下来我们说下对反射方向的重新映射


假设这个方框是反射探头所存储的cubemap,我们处理的像素点在B处,反射方向为BD,我们应当要得到D处的颜色值,然而当我们在采样时,是在反射探头位置采样的也就是A处,这时采样方向还是BD也就是AC,所以采样结果就会是C处的颜色值,就会产生不正确的反射,所以我们需要将反射方向重新映射为AD方向,这样的采样结果就是正确的了.
//重新映射反射方向
inline half3 BoxProjectedDirection(half3 worldRefDir,float3 worldPos,float4 cubemapCenter,float4 boxMin,float4 boxMax)
{
        //使下面的if语句产生分支,定义在HLSLSupport.cginc中
        UNITY_BRANCH
        if(cubemapCenter.w > 0.0)//如果反射探头开启了BoxProjection选项,cubemapCenter.w > 0
        {
                half3 rbmax = (boxMax.xyz - worldPos) / worldRefDir;
                half3 rbmin = (boxMin.xyz - worldPos) / worldRefDir;

                half3 rbminmax = (worldRefDir > 0.0f) ? rbmax : rbmin;

                half fa = min(min(rbminmax.x,rbminmax.y),rbminmax.z);

                worldPos -= cubemapCenter.xyz;
                worldRefDir = worldPos + worldRefDir * fa;
        }
        return worldRefDir;
}关于公式的推导,可以看这里https://blog.csdn.net/puppet_master/article/details/80808486
然后就是对反射探头的采样了
//采样反射探头
//UNITY_ARGS_TEXCUBE定义在HLSLSupport.cginc,用来区别平台
inline half3 SamplerReflectProbe(UNITY_ARGS_TEXCUBE(tex),half3 refDir,half roughness,half4 hdr)
{
        roughness = roughness * (1.7 - 0.7 * roughness);
        half mip = roughness * 6;
        //对反射探头进行采样
        //UNITY_SAMPLE_TEXCUBE_LOD定义在HLSLSupport.cginc,用来区别平台
        half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(tex,refDir,mip);
        //采样后的结果包含HDR,所以我们需要将结果转换到RGB
        //定义在UnityCG.cginc
        return DecodeHDR(rgbm,hdr);
}在光照探头中存储的是一组图像,内容逐渐模糊,之所以这样做是因为当我们的物体比较粗糙的时候,反射的内容也是比较模糊的,如果我们实时模糊运算时,性能肯定是特别费的。所以unity烘焙成一组这样的图像也就是mipmap,然后我们来对其进行不同等级的插值采样。级数越高越模糊,乘以6,也是因为6是我们这个的总级数。因为物体的粗糙度和反射图像的清晰度并不是成线性的,所以会有第一个公式,这个公式是一个近似公式,主要是为了节省性能,你也可以在UnityStandardBRDF.cginc中Unity_GlossyEnvironment函数找到对应的公式。
我们上面只是计算了间接光的直接颜色,这并满足能量守恒,所以我们需要对其物理对其的影响
//计算掠射角时反射率
half grazingTerm = saturate((1 - roughness) + (1-oneMinusReflectivity));
//计算间接光镜面反射
indirectSpecular *= ComputeFresnelLerp(specColor,grazingTerm,nv);
//计算间接光漫反射
indirectDiffuse *= diffColor;在这里镜面反射需要满足菲涅尔反射,因为不同视角他的反射率也不相同。grazingTerm则是我们之前计算的掠射角时反射率,漫反射则是直接乘以他的漫反射率
//计算间接光镜面反射菲涅尔项
inline half3 ComputeFresnelLerp(half3 c0,half3 c1,half cosA)
{
        half t = pow(1 - cosA,5);
        return lerp(c0,c1,t);
}最后只剩下我们的直接光照了,如果你理解了上面的公式,接下来直接带入公式就可以了。
我们先是计算了BRDF的镜面反射项
half V = ComputeSmithJointGGXVisibilityTerm(nl,nv,roughness);//计算BRDF高光反射项,可见性V
half D = ComputeGGXTerm(nh,roughness);//计算BRDF高光反射项,法线分布函数D
half3 F = ComputeFresnelTerm(specColor,lh);//计算BRDF高光反射项,菲涅尔项F

half3 specularTerm = V * D * F;//计算镜面反射项D、F分别为法线分布函数和菲涅尔反射,这两项直接带入公式就可以了。而V这一项被称为可见性项,是阴影遮掩函数G除以BRDF镜面反射分母(n·l)(n·v)部分,在这里为了方便我们也把系数4也加了进去。我们会在分母的后面加上1e-5f,来防止分母为0
//计算Smith-Joint阴影遮掩函数,返回的是除以镜面反射项分母的可见性项V
inline half ComputeSmithJointGGXVisibilityTerm(half nl,half nv,half roughness)
{
        half ag = roughness * roughness;
        half lambdaV = nl * (nv * (1 - ag) + ag);
        half lambdaL = nv * (nl * (1 - ag) + ag);
       
        return 0.5f/(lambdaV + lambdaL + 1e-5f);
}
//计算法线分布函数
inline half ComputeGGXTerm(half nh,half roughness)
{
        half a = roughness * roughness;
        half a2 = a * a;
        half d = (a2 - 1.0f) * nh * nh + 1.0f;
        //UNITY_INV_PI定义在UnityCG.cginc  为1/π
        return a2 * UNITY_INV_PI / (d * d + 1e-5f);
}
//计算菲涅尔
inline half3 ComputeFresnelTerm(half3 F0,half cosA)
{
        return F0 + (1 - F0) * pow(1 - cosA, 5);
}然后是BRDF漫反射部分
half3 diffuseTerm = ComputeDisneyDiffuseTerm(nv,nl,lh,roughness,diffColor);//计算漫反射项也是直接带入了公式
//计算漫反射项
inline half3 ComputeDisneyDiffuseTerm(half nv,half nl,half lh,half roughness,half3 baseColor)
{
        half Fd90 = 0.5f + 2 * roughness * lh * lh;
        return baseColor * UNITY_INV_PI * (1 + (Fd90 - 1) * pow(1-nl,5)) * (1 + (Fd90 - 1) * pow(1-nv,5));
}呼~这样 我们所有的光也就全部都计算完了,就剩最后一步了,全部加起来就好了
//计算最后的颜色
half3 color = UNITY_PI * (diffuseTerm + specularTerm) * _LightColor0.rgb * nl * atten
                                + indirectDiffuse + indirectSpecular + emission;

//设置雾效,定义在UnityCG.cginc
UNITY_APPLY_FOG(i.fogCoord, color.rgb);我们将计算好BRDF带入了上面的渲染方程,并添加了阴影和光衰atten对其的影响,然后加上了间接光的两项和自发光,最后应用了雾效的影响。然后color就是我们最后的颜色。
源码

源码和使用的纹理放在了百度网盘里
链接:https://pan.baidu.com/s/1leeHqASBllJhMSdFSZ19kg
提取码:fvjf
纹理来源:Rusted Iron PBR Metal Material Alt - Free PBR Materials
感觉效果还是挺不错的,哈哈

本帖子中包含更多资源

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

×
发表于 2022-1-4 10:15 | 显示全部楼层
加油
 楼主| 发表于 2022-1-4 10:16 | 显示全部楼层
感谢分享
[棒]
发表于 2022-1-4 10:24 | 显示全部楼层
高手,能加个微信吗?有一些问题请教
发表于 2022-1-4 10:25 | 显示全部楼层
你的高光项为什么不除以4 * nl * nv?
发表于 2022-1-4 10:31 | 显示全部楼层
他和阴影遮掩函数放到一起计算了
发表于 2022-1-4 10:38 | 显示全部楼层
源码还有吗
发表于 2022-1-4 10:39 | 显示全部楼层
那个链接应该没有失效吧,另外我也丢到了github上了。https://github.com/Straw1997/StudyPBR
发表于 2022-1-4 10:42 | 显示全部楼层
完美,非常大神感谢分享
发表于 2022-1-4 10:50 | 显示全部楼层
小白想问一下为什么V、D分母项都加了1e-5?看了毛星云大大总结的smith公式里都没有这一项
[疑惑]
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

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

GMT+8, 2024-12-22 15:46 , Processed in 0.201093 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

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