pbr发光 unity_【学习笔记】Unity PBR的实现

这篇博客详细介绍了在Unity中实现PBR(物理渲染)的过程,包括使用的公式、BRDF的各个组成部分以及间接光照的计算。作者通过源码分析,解释了镜面反射、漫反射、菲涅尔反射等概念,并提供了实现PBR shader的步骤和关键函数。博客末尾还分享了源码链接。
摘要由CSDN通过智能技术生成

前言

哈哈,终于看完了Standard的源码了(emmm一部分),再结合之前学习PBR的知识,现在感觉浑身充满了能量,便决定自己实现一个PBR的shader,来熟悉一下,若有什么不对还请大佬指教...

感觉和大佬们差了好几个次元,究竟什么时候能够和大佬一起看太阳...

关于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.cgincUNITY_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.cginco.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.cgincTRANSFER_SHADOW(o);

//填充雾效所需要的参数,定义在UnityCG.cgincUNITY_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_ONambientOrLightmapUV.xy = uv1.xy * unity_LightmapST.xy + unity_LightmapST.zw;

//仅对动态物体采样光照探头,定义在UnityCG.cginc#elif UNITY_SHOULD_SAMPLE_SH//计算非重要的顶点光照#ifdef VERTEXLIGHT_ON//计算4个顶点光照,定义在UnityCG.cgincambientOrLightmapUV.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.cgincambientOrLightmapUV.rgb += ShadeSH9(half4(worldNormal,1));

#endif

//如果开启了 动态光照贴图,计算动态光照贴图的uv坐标#ifdef DYNAMICLIGHTMAP_ONambientOrLightmapUV.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.cginchalf3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));//世界空间下的观察方向,定义在UnityCG.cginchalf3 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_SHindirectDiffuse = ambientOrLightmapUV.rgb;

#endif

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

#endif#ifdef DYNAMICLIGHTMAP_ON//对动态光照贴图进行采样和解码//DecodeRealtimeLightmap定义在UnityCG.cgincindirectDiffuse += 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],在其他平台上什么事都不做。关于这个更多的信息可以看这里

#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;

}

然后就是对反射探头的采样了

//采样反射探头//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.cgincreturn 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高光反射项,可见性Vhalf D = ComputeGGXTerm(nh,roughness);//计算BRDF高光反射项,法线分布函数Dhalf3 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阴影遮掩函数,返回的是除以镜面反射项分母的可见性项Vinline 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.cgincUNITY_APPLY_FOG(i.fogCoord, color.rgb);

我们将计算好BRDF带入了上面的渲染方程,并添加了阴影和光衰atten对其的影响,然后加上了间接光的两项和自发光,最后应用了雾效的影响。然后color就是我们最后的颜色。

源码

源码和使用的纹理放在了百度网盘里

提取码:fvjf

感觉效果还是挺不错的,哈哈

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值