|
前言
哈哈,终于看完了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
感觉效果还是挺不错的,哈哈
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|