目的
PBR(Physical Based Rendering) 可以说是很多游戏开发的标配了,理解Unity内部PBR的实现,对我们来说有几点意义:
- 可以让我们对Unity内部材质各种参数的调节有个 更理性的认识 ;
- 可以在其它建模工具中进行此 PBR的插件实现 ,使得建模时就能看到Unity引擎中的光照效果;
- 可以让我们对PBR关照有个系统的认识,便于对其进行 改进(估计要读好几篇论文==);
- 等等
前言
我们采用 Unity2018.3.6版本的内置Shader 来进行分析,Unity官方提供各个版本的内置Shader源码,下载连接在 这里 ;
Unity内置着色器还是挺复杂的,在常用的“Standard” Shader中便包括了前向光照、延迟光照、全局光照等多种光照模块,我们这里只针对 前向光照(forward rendering) 来进行分析;
由于其源码的复杂性,不太建议对Unity Shader掌握不熟悉的人直接去看,这里的熟悉包括 Shader的语法 ,以及Unity对Shader的包装语法,另外还有 常见的光照类型 等其它知识(厉害的人边看边查文档到也还行==)。
文章前半部分对源码框架进行了剖析,想要 直接使用光照算法 的可直接跳到 总结部分 ;
Standard Shader框架
Unity内置Standard Shader的框架 如下代码所示,各部分功能为:
Shader "Standard"
{
Properties
{
...
}
CGINCLUDE
#define UNITY_SETUP_BRDF_INPUT MetallicSetup
ENDCG
SubShader //LOD 300
{
...
}
SubShader //LOD 300
{
...
}
FallBack "VertexLit"
CustomEditor "StandardShaderGUI"
}
- Properties为该Shader中所使用的可调节属性,对应于引擎中的这个界面
;
SubShader //LOD 300
为Lod Of Detail大于等于300情况下所使用的SubShader,SubShader //LOD 15
0同理,参考这里;FallBack "VertexLit"
表示Lod Of Detail小于150时,所使用的顶点关照,参考这里;CustomEditor "StandardShaderGUI"
表示采用StandardShaderGUI来Unity中显示Shader属性,参考这里;
由于SubSahder //LOD 300
模块下的内容比较全面,包含了高LOD下更加真实的光照实现,因此我们针对此模块来进行分析,该 SubSahder 模块下的框架图 如代码所示。
SubShader
{
Tags {
"RenderType"="Opaque" "PerformanceChecks"="False" }
LOD 300
// Base forward pass (directional light, emission, lightmaps, ...)
Pass
{
...
}
// Additive forward pass (one light per pass)
Pass
{
...
}
// Shadow rendering pass
{
...
}
// Deferred pass
{
...
}
// Extracts information for lightmapping, GI (emission, albedo, ...)
// This pass it not used during regular rendering.
{
...
}
}
根据注释可以知道:
- Pass1针对平行光的Forward Rendering;
- Pass2针对普通光源的Forward Rendering;
- Pass3针对Shadow Rendering;
- Pass4针对Deferred Rendering;
- Pass5针对Global Illumination;
由于我们主要学习的PBR光照的实现,并不针对渲染流程进行研究,因此我们 针对Pass1进行研究 ,Pass2与Pass1研究方法类似,所以就不费更多篇幅。 Pass1的框架图 如下代码所示,
Pass
{
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase" }
Blend [_SrcBlend] [_DstBlend]
ZWrite [_ZWrite]
CGPROGRAM
#pragma target 3.0
// -------------------------------------
#pragma shader_feature _NORMALMAP
#pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON
#pragma shader_feature _EMISSION
#pragma shader_feature _METALLICGLOSSMAP
#pragma shader_feature ___ _DETAIL_MULX2
#pragma shader_feature _ _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
#pragma shader_feature _ _SPECULARHIGHLIGHTS_OFF
#pragma shader_feature _ _GLOSSYREFLECTIONS_OFF
#pragma shader_feature _PARALLAXMAP
#pragma multi_compile_fwdbase
#pragma multi_compile_fog
#pragma multi_compile_instancing
// Uncomment the following line to enable dithering LOD crossfade. Note: there are more in the file to uncomment for other passes.
//#pragma multi_compile _ LOD_FADE_CROSSFADE
#pragma vertex vertBase
#pragma fragment fragBase
#include "UnityStandardCoreForward.cginc"
ENDCG
}
可以看着这里有很多宏定义,但是跟我们所研究的 PBR光照相关的代码 为:
#pragma vertex vertBase
#pragma fragment fragBase
#include "UnityStandardCoreForward.cginc"
Pass2中的相关代码为:
#pragma vertex vertAdd
#pragma fragment fragAdd
#include "UnityStandardCoreForward.cginc"
即, 顶点着色器入口函数为"vertBase",片段着色器入口函数为"fragBase",且这些函数实现在"UnityStandardCoreForward.cginc"文件中 。因此我们的目标就是到"UnityStandardCoreForward.cginc"文件中查看"vertBase"与"fragBase"函数的实现,其 文件中代码实现部分 为:
#if UNITY_STANDARD_SIMPLE
#include "UnityStandardCoreForwardSimple.cginc"
VertexOutputBaseSimple vertBase (VertexInput v) {
return vertForwardBaseSimple(v); }
VertexOutputForwardAddSimple vertAdd (VertexInput v) {
return vertForwardAddSimple(v); }
half4 fragBase (VertexOutputBaseSimple i) : SV_Target {
return fragForwardBaseSimpleInternal(i); }
half4 fragAdd (VertexOutputForwardAddSimple i) : SV_Target {
return fragForwardAddSimpleInternal(i); }
#else
#include "UnityStandardCore.cginc"
VertexOutputForwardBase vertBase (VertexInput v) {
return vertForwardBase(v); }
VertexOutputForwardAdd vertAdd (VertexInput v) {
return vertForwardAdd(v); }
half4 fragBase (VertexOutputForwardBase i) : SV_Target {
return fragForwardBaseInternal(i); }
half4 fragAdd (VertexOutputForwardAdd i) : SV_Target {
return fragForwardAddInternal(i); }
#endif
由此可以知道针对平行光的 PBR光照实现分为两种情况,一种是简化版的实现,位于"UnityStandardCoreForwardSimple.cginc"文件中,一种是标准版的实现,位于"UnityStandardCore.cginc"文件中 。代码中的"vertAdd"函数与"fragAdd"函数实际为Pass2中针对普通光源所采用的顶点与片段函数。这里我们只 针对"UnityStandardCore.cginc"文件来查看"vertForwardBase"函数与"fragForwardBaseInternal"函数的实现,即标准版平行光光照下PBR的实现 。
最终我们打开"UnityStandardCore.cginc"文件终于看到了具体的实现,而不再是一吨宏定义绕来绕去。。。具体内容在下面讲解。
Vertex Shader的实现
为了便于理解,我在代码中进行了必要的注释
其实现代码为:
VertexOutputForwardBase vertForwardBase (VertexInput v)
{
UNITY_SETUP_INSTANCE_ID(v);
VertexOutputForwardBase o;
UNITY_INITIALIZE_OUTPUT(VertexOutputForwardBase, o);
UNITY_TRANSFER_INSTANCE_ID(v, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
//将顶点坐标从局部坐标系转换到裁剪坐标系
float4 posWorld = mul(unity_ObjectToWorld, v.vertex);
#if UNITY_REQUIRE_FRAG_WORLDPOS
#if UNITY_PACK_WORLDPOS_WITH_TANGENT
o.tangentToWorldAndPackedData[0].w = posWorld.x;
o.tangentToWorldAndPackedData[1].w = posWorld.y;
o.tangentToWorldAndPackedData[2].w = posWorld.z;
#else
o.posWorld = posWorld.xyz;
#endif
#endif
o.pos = UnityObjectToClipPos(v.vertex);
//纹理坐标获取
o.tex = TexCoords(v);
//视线方向获取
o.eyeVec.xyz = NormalizePerVertexNormal(posWorld.xyz - _WorldSpaceCameraPos);
//法线从从局部坐标系转换到世界坐标系
float3 normalWorld = UnityObjectToWorldNormal(v.normal);
//Tangent向量从从局部坐标系转换到世界坐标系
#ifdef _TANGENT_TO_WORLD
float4 tangentWorld = float4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w);
float3x3 tangentToWorld = CreateTangentToWorldPerVertex(normalWorld, tangentWorld.xyz, tangentWorld.w);
o.tangentToWorldAndPackedData[0].xyz = tangentToWorld[0];
o.tangentToWorldAndPackedData[1].xyz = tangentToWorld[1];
o.tangentToWorldAndPackedData[2].xyz = tangentToWorld[2];
#else
o.tangentToWorldAndPackedData[0].xyz = 0;
o.tangentToWorldAndPackedData[1].xyz = 0;
o.tangentToWorldAndPackedData[2].xyz = normalWorld;
#endif
//We need this for shadow receving
UNITY_TRANSFER_LIGHTING(o, v.uv1);
//获取LightMap对应纹理坐标
o.ambientOrLightmapUV = VertexGIForward(v, posWorld, normalWorld);
//视差贴图Tangent向量的变换
#ifdef _PARALLAXMAP
TANGENT_SPACE_ROTATION;
half3 viewDirForParallax = mul (rotation, ObjSpaceViewDir(v.vertex));
o.tangentToWorldAndPackedData[0].w = viewDirForParallax.x;
o.tangentToWorldAndPackedData[1].w = viewDirForParallax.y;
o.tangentToWorldAndPackedData[2].w = viewDirForParallax.z;
#endif
//雾效相关参数的计算
UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC(o,o.pos);
return o;
}
可以看到,顶点着色器主要进行相关顶点坐标、法线、纹理坐标、tangent向量的变换操作,没有进行实际的着色计算。
Fragment Shader的实现
仍然对主要的模块进行注释:
half4 fragForwardBaseInternal (VertexOutputForwardBase i)
{
UNITY_APPLY_DITHER_CROSSFADE(i.pos.xy);
FRAGMENT_SETUP(s)
UNITY_SETUP_INSTANCE_ID(i);
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
//计算光源的衰减效果
UnityLight mainLight = MainLight ();
UNITY_LIGHT_ATTENUATION(atten, i, s.posWorld);
//计算环境光遮蔽与全局光照效果
half occlusion = Occlusion(i.tex.xy);
UnityGI gi = FragmentGI (s, occlusion, i.ambientOrLightmapUV, atten, mainLight);
//计算材质的PBS光照效果
half4 c = UNITY_BRDF_PBS (s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect);
c.rgb += Emission(i.tex.xy);
//雾效的计算
UNITY_EXTRACT_FOG_FROM_EYE_VEC(i);
UNITY_APPLY_FOG(_unity_fogCoord, c.rgb);
return OutputForward (c, s.alpha);
}
可以看到材质PBS光照效果的计算实际上只有倒数第二块那一部分内容,UNITY_BRDF_PBS宏的实现在"UnityPBSLighting.cginc"文件中,其代码为:
// Default BRDF to use:
#if !defined (UNITY_BRDF_PBS) // allow to explicitly override BRDF in custom shader
// still add safe net for low shader models, otherwise we might end up with shaders failing to compile
#if SHADER_TARGET < 30 || defined(SHADER_TARGET_SURFACE_ANALYSIS) // only need "something" for surface shader analysis pass; pick the cheap one
#define UNITY_BRDF_PBS BRDF3_Unity_PBS
#elif defined(UNITY_PBS_USE_BRDF3)
#define UNITY_BRDF_PBS BRDF3_Unity_PBS
#elif defined(UNITY_PBS_USE_BRDF2)
#define UNITY_BRDF_PBS BRDF2_Unity_PBS
#elif defined(UNITY_PBS_USE_BRDF1)
#define UNITY_BRDF_PBS BRDF1_Unity_PBS
#else
#error something broke in auto-choosing BRDF
#endif
#endif
可以看到,在Unity内部,其BRDF的实现也有不同的版本,BRDF1_Unity_PBS、BRDF2_Unity_PBS、BRDF3_Unity_PBS三个函数的实现在"UnityStandardBRDF.cginc"文件中可以查找到,我们针对 BRDF1_Unity_PBS 进行分析即可,其它的函数实现与其类似。
其具体代码为:
// Main Physically Based BRDF
// Derived from Disney work and based on Torrance-Sparrow micro-facet model
//
// BRDF = kD / pi + kS * (D * V * F) / 4
// I = BRDF * NdotL
//
// * NDF (depending on UNITY_BRDF_GGX):
// a) Normalized BlinnPhong
// b) GGX
// * Smith for Visiblity term
// * Schlick approximation for Fresnel
half4 BRDF1_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness,
float3 normal, float3 viewDir,
UnityLight light, UnityIndirect gi)
{
float perceptualRoughness = SmoothnessToPerceptualRoughness (smoothness);
float3 halfDir = Unity_SafeNormalize (float3(light.dir) + viewDir);
#define UNITY_HANDLE_CORRECTLY_NEGATIVE_NDOTV 0
#if UNITY_HANDLE_CORRECTLY_NEGATIVE_NDOTV
half shiftAmount = dot(normal, viewDir);
normal = shiftAmount < 0.0f ? normal + viewDir * (-shiftAmount + 1e-5f) : normal;
float nv = saturate(dot(normal, viewDir))