学习笔记-Unity实现PBR
知乎上有许多大佬其实已经总结过了在Unity实现多种PBR材质的方法,我作为一名还在学习的小菜鸡,这篇文章只是一个学习笔记,欢迎大佬们纠正错误^_^本文尝试实现的是COD里的PBR材质。整体效果
首先是核心的渲染方程:
这个公式无需赘述,有很多大佬已经对其进行了解释,一个点的亮度收到了许多不同的入射光Li(Wi)不同影响的累加效果,对其进行无穷小的累加是对该点进行半球积分。
其次是BRDF,双向反射分布函数,是一个用来描述表面如何反射光线的方程,其进阶还有BTDF是一个用来描述物体如何投射光线的方程,BSDF=BRDF+BTDF, 业界广泛采用的公式是基于微平面理论的:Microfacet Cook-Torrance BRDF形式
其中F项代表菲涅尔函数,D项为法线分布函数,G项为几何函数,这里是直接光的镜面反射项的使用,实际的效果应该是镜面反射项+漫反射项。
镜面反射项:
菲涅尔函数:
F项:
使用公式:
代码:
half3 F0 = lerp(half3(0.04,0.04,0.04),BaseColor,Metallic);
half3 Fresnel = F0 + (1 - F0) * pow( 1 - saturate(dot(LightDir,H)), 5);使用的是Schlick近似法,光线以不同的角度射向物体也就是入射会有不同的反射率,相同的入射角度也会有不同的反射率。F0是0度角入射的菲涅尔反射值。
法线分布函数:
D项:
使用公式:
代码:
float n = pow(2,13*Gloss);
float PI = 3.1415926;
float NormalizedFactor = (n+2)/(2*PI);
float NormalizedBlin = dot(H,Worldnormal);
NormalizedBlin = pow(NormalizedBlin,n) * NormalizedFactor;
Blinn-Phong分布,其中n是Blinn-PhongNDF的粗糙度参数,高值表示光滑表面,低值表示粗糙表面。在COD:Black Ops中其被设为8192。
阴影遮挡函数:
G项:
严格意义上的应该称它为几何函数,是保证MicrofacetBRDF理论上能量守恒,逻辑上自洽的重要一环,描述了微平面自阴影的属性,表示具有半矢量法线的微平面中,同时被入射方向和反射方向可见,也就是没有被遮挡的比例,也是没有被遮挡m=h微表面的百分比。其实现形式是多种多样的,从Cook-Torrance GSF, Neumann GSF等等到分离遮蔽阴影类型的SmithGGX导出的G项,Smith-BeckmanGSF, Shlick-BeckmanGSF,这里实现的是分离遮蔽阴影类型的。
公式:
代码:
float Vis_SmithJointApprox(float a2, float NoV, float NoL )
{
float a = sqrt(a2);
float Vis_SmithV = NoL * ( NoV * (1 - a) + a );
float Vis_SmithL = NoV * ( NoL * (1 - a) + a );
return 0.5 * rcp( Vis_SmithV + Vis_SmithL );
}漫反射项:
COD的PBRshader与UnityStandardShader在直接光漫反射部分都是相同的,都是采用了一个Lambert光照模型,并且都在下面除了一个PI,这个PI的作用是保证能量守恒。
half3 DiffuseColor = lerp(BaseColor,half3(0,0,0), Metallic)/3.14;
half lambert = max(0, dot(Worldnormal,LightDir));
float3 Final = lambert*DiffuseColo间接光照:
间接高光:IBL的意思是把光照部分烘焙到图像中,烘焙出多个粗糙度下的环境图,对其进行采样。这种方式可以有效地捕捉环境的全局光照和氛围,使物体更好地融入其环境。
间接漫反射:环境贴图cubemap积分成模糊的全局光照贴图,再将全局光照贴图投影到球谐光照的基函数上储存。
总结:
总的来说PBR实现的方式有非常多种,只需要计算出直接光照,间接光照,自发光,最后的光照效果也是计算完这三个光加起来就可以了,其直接光照的计算方式是多种多样的,TA在项目中需要考虑到游戏引擎,光照环境,项目性能开销,游戏整体美术效果与氛围来做出决定。掌握PBR的实现是作为一个TA的基础要求,TA新手并不一定需要去完全理解这些复杂数学公式其中的原理,只需要掌握其中的基础知识,并知道这些公式在是用来做什么的,并将它们使用代码实现出来即可,随着学习工作的时间增加,将其完全掌握。
完整代码:
Shader "PBR/PBR_02"
{
Properties
{
_MainTex ("Texture1", 2D) = "blck"{}
BaseColor ("BaseColor", Color) = (1,1,1,1)
Gloss("Gloss",Range(0,1))=0.5
Metallic("Metallic", Range(0,1)) = 0
}
SubShader
{
pass
{
Tags {"LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex VS
#pragma fragment PS
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#include "Lighting.cginc"
struct vertexInput
{
float4 vertex:POSITION;
float3 normal:NORMAL;
};
struct VertexToPs
{
float4 NDCPos : SV_POSITION;
float3 Worldnormal : TEXCOORD0;
float3 WorldPos : TEXCOORD1;
float3 GI : TEXCOORD2;
};
VertexToPs VS(vertexInput input)
{
VertexToPs O;
O.NDCPos = UnityObjectToClipPos(input.vertex.xyz);
O.Worldnormal = UnityObjectToWorldNormal(input.normal.xyz);
O.WorldPos = mul(unity_ObjectToWorld,input.vertex);
O.GI = ShadeSH9(float4(O.Worldnormal,1));
return O;
}
float3 BaseColor;
float Gloss;
float Metallic;
float GeometrySchlickGGX(float NdotV, float roughness)
{
float r = (roughness + 1.0);
float k = (r*r) / 8.0;
float nom = NdotV;
float denom = NdotV * (1.0 - k) + k;
return nom / denom;
}
float GeometrySmith(half3 N, half3 V, half3 L,float roughness)
{
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float ggx2 = GeometrySchlickGGX(NdotV, roughness);
float ggx1 = GeometrySchlickGGX(NdotL, roughness);
return ggx1 * ggx2;
}
float Vis_SmithJointApprox(float a2, float NoV, float NoL )
{
float a = sqrt(a2);
float Vis_SmithV = NoL * ( NoV * (1 - a) + a );
float Vis_SmithL = NoV * ( NoL * (1 - a) + a );
return 0.5 * rcp( Vis_SmithV + Vis_SmithL );
}//阴影-遮挡函数
float V_SmithGGXCorrelated(float NoV, float NoL, float a)
{
float a2 = a * a;
float GGXL = NoV * sqrt((-NoL * a2 + NoL) * NoL + a2);
float GGXV = NoL * sqrt((-NoV * a2 + NoV) * NoV + a2);
return 0.5 / (GGXV + GGXL);
}
float4 PS(VertexToPs input):SV_TARGET
{
float3 LightDir = _WorldSpaceLightPos0.xyz;
LightDir = normalize(LightDir);
float3 Worldnormal = input.Worldnormal;
Worldnormal = normalize(Worldnormal);
float3 VDir = UnityWorldSpaceViewDir(input.WorldPos);
VDir = normalize(VDir);
float3 H = LightDir + VDir;
H = normalize(H);
float Blin = dot(H,Worldnormal);
float n = pow(2,13*Gloss);
float PI = 3.1415926;
float NormalizedFactor = (n+2)/(2*PI);
float NormalizedBlin = dot(H,Worldnormal);
NormalizedBlin = pow(NormalizedBlin,n) * NormalizedFactor;
half3 DiffuseColor = lerp(BaseColor,half3(0,0,0), Metallic)/3.14;
float3 R = reflect(-LightDir, Worldnormal);
R = normalize(R);
half lambert = max(0, dot(Worldnormal,LightDir));
//菲涅尔函数
half3 F0 = lerp(half3(0.04,0.04,0.04),BaseColor,Metallic);
half3 Fresnel = F0 + (1 - F0) * pow( 1 - saturate(dot(LightDir,H)), 5);
half a = 1/sqrt(PI/4 * (1 - Gloss) + PI/2);
half NoL = saturate(dot(LightDir,Worldnormal));
half NoV = saturate(dot(Worldnormal,VDir));
half VisV = NoV * (1 - a) + a;
half VisL = NoL * (1 - a) + a;
half V = 1/(VisV * VisL)/(4 * NoL * NoV);
float3 Final = lambert*DiffuseColor+max(0,NormalizedBlin*Fresnel*V*NoL);
float3 reflectionDir = reflect(-VDir , Worldnormal);
Unity_GlossyEnvironmentData envData;
envData.roughness = 1 - Gloss;
envData.reflUVW = reflectionDir;
float3 specular = Unity_GlossyEnvironment(UNITY_PASS_TEXCUBE(unity_SpecCube0), unity_SpecCube0_HDR, envData);
Final += specular * F0;
float3 GI = ShadeSH9(float4(Worldnormal,1));
Final = Final + GI * DiffuseColor * 3.14;
return float4(Final,1);
}
ENDCG
}
}
}
页:
[1]