赞
踩
PBR(Physical Based Rendering) 可以说是很多游戏开发的标配了,理解Unity内部PBR的实现,对我们来说有几点意义:
我们采用 Unity2018.3.6版本的内置Shader 来进行分析,Unity官方提供各个版本的内置Shader源码,下载连接在 这里 ;
Unity内置着色器还是挺复杂的,在常用的“Standard” Shader中便包括了前向光照、延迟光照、全局光照等多种光照模块,我们这里只针对 前向光照(forward rendering) 来进行分析;
由于其源码的复杂性,不太建议对Unity Shader掌握不熟悉的人直接去看,这里的熟悉包括 Shader的语法 ,以及Unity对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" }
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. { ... } }
根据注释可以知道:
由于我们主要学习的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"文件终于看到了具体的实现,而不再是一吨宏定义绕来绕去。。。具体内容在下面讲解。
为了便于理解,我在代码中进行了必要的注释
其实现代码为:
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向量的变换操作,没有进行实际的着色计算。
仍然对主要的模块进行注释:
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)); // TODO: this saturate should no be necessary here #else half nv = abs(dot(normal, viewDir)); // This abs allow to limit artifact #endif float nl = saturate(dot(normal, light.dir)); float nh = saturate(dot(normal, halfDir)); half lv = saturate(dot(light.dir, viewDir)); half lh = saturate(dot(light.dir, halfDir)); // Diffuse term half diffuseTerm = DisneyDiffuse(nv, nl, lh, perceptualRoughness) * nl; float roughness = PerceptualRoughnessToRoughness(perceptualRoughness); #if UNITY_BRDF_GGX // GGX with roughtness to 0 would mean no specular at all, using max(roughness, 0.002) here to match HDrenderloop roughtness remapping. roughness = max(roughness, 0.002); float V = SmithJointGGXVisibilityTerm (nl, nv, roughness); float D = GGXTerm (nh, roughness); #else // Legacy half V = SmithBeckmannVisibilityTerm (nl, nv, roughness); half D = NDFBlinnPhongNormalizedTerm (nh, PerceptualRoughnessToSpecPower(perceptualRoughness)); #endif float specularTerm = V*D * UNITY_PI; // Torrance-Sparrow model, Fresnel is applied later # ifdef UNITY_COLORSPACE_GAMMA specularTerm = sqrt(max(1e-4h, specularTerm)); # endif // specularTerm * nl can be NaN on Metal in some cases, use max() to make sure it's a sane value specularTerm = max(0, specularTerm * nl); #if defined(_SPECULARHIGHLIGHTS_OFF) specularTerm = 0.0; #endif // surfaceReduction = Int D(NdotH) * NdotH * Id(NdotL>0) dH = 1/(roughness^2+1) half surfaceReduction; # ifdef UNITY_COLORSPACE_GAMMA surfaceReduction = 1.0-0.28*roughness*perceptualRoughness; // 1-0.28*x^3 as approximation for (1/(x^4+1))^(1/2.2) on the domain [0;1] # else surfaceReduction = 1.0 / (roughness*roughness + 1.0); // fade \in [0.5;1] # endif // To provide true Lambert lighting, we need to be able to kill specular completely. specularTerm *= any(specColor) ? 1.0 : 0.0; half grazingTerm = saturate(smoothness + (1-oneMinusReflectivity)); half3 color = diffColor * (gi.diffuse + light.color * diffuseTerm) + specularTerm * light.color * FresnelTerm (specColor, lh) + surfaceReduction * gi.specular * FresnelLerp (specColor, grazingTerm, nv); return half4(color, 1); }
由最后的一部分代码
half3 color = diffColor * (gi.diffuse + light.color * diffuseTerm)
+ specularTerm * light.color * FresnelTerm (specColor, lh)
+ surfaceReduction * gi.specular * FresnelLerp (specColor, grazingTerm, nv);
可以看出, Unity内部的PBR光照也是采用漫反射 + 全局漫反射 + 高光反射 + 全局高光反射项来组成的 ;全局反射项的计算使用的是IBL(image based lighting)计算方法,其计算方法与直接光源的计算方法稍有不同,这里不进行详细介绍,而是重点介绍漫反射与高光反射,两者属于基础的光照理论计算方法。
漫反射项为diffColor*light.color * diffuseTerm;
高光反射项为specularTerm * light.color * FresnelTerm (specColor, lh);
diffColor*light.color * diffuseTerm计算公式中,最重要的是 diffuseTerms项 的计算,参考此连接5.3部分,从代码中可以看到:
/*式中:
n : normal;
v : view direction;
l : light direction;
h : half direction;即normalize(n+l);
两个字母即表示两个方向的点积,即nv = dot(normal, view);
perceptualRoughness表示0-1范围的粗糙度;
*/
half diffuseTerm = DisneyDiffuse(nv, nl, lh, perceptualRoughness) * nl;
可知,diffuseTerm的计算,Unity主要采用迪斯尼所使用的计算方法, DisneyDiffuse函数的具体代码 为:
half DisneyDiffuse(half NdotV, half NdotL, half LdotH, half perceptualRoughness)
{
half fd90 = 0.5 + 2 * LdotH * LdotH * perceptualRoughness;
// Two schlick fresnel term
half lightScatter = (1 + (fd90 - 1) * Pow5(1 - NdotL));
half viewScatter = (1 + (fd90 - 1) * Pow5(1 - NdotV));
return lightScatter * viewScatter;
}
所采用的公式对应为文献中的:
f
d
=
b
a
s
e
C
o
l
o
r
π
∗
(
1
+
(
F
d
90
−
1
)
∗
(
1
−
c
o
s
θ
l
)
5
)
(
1
+
(
F
d
90
−
1
)
∗
(
1
−
c
o
s
θ
v
)
5
)
F
d
90
=
0.5
+
2
c
o
s
θ
d
2
r
o
u
g
h
n
e
s
s
f_d = \frac {baseColor} \pi *(1 + (F_{d90} - 1)*(1 - cos\theta_l )^5)(1 + (F_{d90} - 1)*(1 - cos\theta_v)^5) \\ F_{d90} = 0.5 + 2cos\theta_d ^2roughness
fd=πbaseColor∗(1+(Fd90−1)∗(1−cosθl)5)(1+(Fd90−1)∗(1−cosθv)5)Fd90=0.5+2cosθd2roughness
按照迪斯尼的说法, 这个模型有更好的艺术友好性,而不是严格的遵从物理规律。即便如此,该模型也比普通的Lambert模型要复杂的多,因为其考虑了菲尼尔现象以及粗糙度的影响,并且能够更好的吻合实验数据 。以后在使用漫反射模型的情况下,可以考虑使用此模型来代替Lambert模型,来获取更好的效果。
在高光项的计算公式中,主要是 specularTerm 与 FresnelTerm 的计算。
FresnelTerm为高光反射模型中常见的 菲涅尔项 ,其实现为常见的 Schlick Fresnel approximation ,即:
inline half3 FresnelTerm (half3 F0, half cosA)
{
half t = Pow5 (1 - cosA); // ala Schlick interpoliation
return F0 + (1-F0) * t;
}
计算公式为:
F
S
c
h
l
i
c
k
=
F
0
+
(
1
−
F
0
)
(
1
−
c
o
s
θ
d
)
5
F_{Schlick}=F_0+(1-F_0)(1-cos\theta_d)^5
FSchlick=F0+(1−F0)(1−cosθd)5
其中, F0为光线垂直入射时,该材质的反射光线所占出射光线的比例 (另一部分发生折射或吸收)。
实际上 specularTerm*FresnelTerm 为高光反射BRDF,此BRDF采用的是微表面模型(Microfacet),可参考这里,其计算公式为下式的右部分:
f
(
l
,
v
)
=
d
i
f
f
u
s
e
+
D
(
θ
h
)
F
(
θ
d
)
G
(
θ
l
,
θ
v
)
4
c
o
s
(
θ
l
)
c
o
s
(
θ
v
)
f(\bold {l,v})=diffuse + \frac {D(\theta_h)F(\theta_d)G(\theta_l,\theta_v)} {4cos(\theta_l)cos(\theta_v)}
f(l,v)=diffuse+4cos(θl)cos(θv)D(θh)F(θd)G(θl,θv)
FresnelTerm对应了F项,specularTerm对应了
D
(
θ
h
)
G
(
θ
l
,
θ
v
)
4
c
o
s
(
θ
l
)
c
o
s
(
θ
v
)
\frac {D(\theta_h)G(\theta_l,\theta_v)} {4cos(\theta_l)cos(\theta_v)}
4cos(θl)cos(θv)D(θh)G(θl,θv);而 在Unity内部的实现中,specularTerm 为:
float specularTerm = V*D * UNITY_PI;
其中 V对应了G项,D对应D项,并且1/(4*cos(θl)*cos(θv))项被整合至了V项中;
这样 specularTerm的计算就转化为V、D的计算,Unity中这两项的计算代码 为:
#if UNITY_BRDF_GGX
// GGX with roughtness to 0 would mean no specular at all, using max(roughness, 0.002) here to match HDrenderloop roughtness remapping.
roughness = max(roughness, 0.002);
float V = SmithJointGGXVisibilityTerm (nl, nv, roughness);
float D = GGXTerm (nh, roughness);
#else
// Legacy
half V = SmithBeckmannVisibilityTerm (nl, nv, roughness);
half D = NDFBlinnPhongNormalizedTerm (nh, PerceptualRoughnessToSpecPower(perceptualRoughness));
#endif
可以看到 最新的计算模型为GGX模型,而遗留计算模型为BlinnPhong模型 ;我们这里研究GGX模型,因为GGX模型更加高级且更加真实,BlinnPhong模型大家自己类比查看源码即可;
GGX模型中V项的计算代码为:
// Ref: http://jcgt.org/published/0003/02/03/paper.pdf inline float SmithJointGGXVisibilityTerm (float NdotL, float NdotV, float roughness) { #if 0 // Original formulation: // lambda_v = (-1 + sqrt(a2 * (1 - NdotL2) / NdotL2 + 1)) * 0.5f; // lambda_l = (-1 + sqrt(a2 * (1 - NdotV2) / NdotV2 + 1)) * 0.5f; // G = 1 / (1 + lambda_v + lambda_l); // Reorder code to be more optimal half a = roughness; half a2 = a * a; half lambdaV = NdotL * sqrt((-NdotV * a2 + NdotV) * NdotV + a2); half lambdaL = NdotV * sqrt((-NdotL * a2 + NdotL) * NdotL + a2); // Simplify visibility term: (2.0f * NdotL * NdotV) / ((4.0f * NdotL * NdotV) * (lambda_v + lambda_l + 1e-5f)); return 0.5f / (lambdaV + lambdaL + 1e-5f); // This function is not intended to be running on Mobile, // therefore epsilon is smaller than can be represented by half #else // Approximation of the above formulation (simplify the sqrt, not mathematically correct but close enough) float a = roughness; float lambdaV = NdotL * (NdotV * (1 - a) + a); float lambdaL = NdotV * (NdotL * (1 - a) + a); #if defined(SHADER_API_SWITCH) return 0.5f / (lambdaV + lambdaL + 1e-4f); // work-around against hlslcc rounding error #else return 0.5f / (lambdaV + lambdaL + 1e-5f); #endif #endif }
嗯…很复杂,但是注释还是很清楚的,其具体采用的是 Smith joint masking-shadowing function,V(G)项的具体的计算公式 为:
lambda_v = (-1 + sqrt(a2 * (1 - NdotL2) / NdotL2 + 1)) * 0.5f;
lambda_l = (-1 + sqrt(a2 * (1 - NdotV2) / NdotV2 + 1)) * 0.5f;
G = 1 / (1 + lambda_v + lambda_l);
该公式的具体来源可参考这里,由于计算公式的复杂性,Unity的应用代码中对齐进行的简化,具体使用公式为:
lambda_v = (-1 + sqrt(a2 * (1 - NdotL2) / NdotL2 + 1)) * 0.5f;
lambda_l = (-1 + sqrt(a2 * (1 - NdotV2) / NdotV2 + 1)) * 0.5f;
G = 0.5f / (lambdaV + lambdaL + 1e-5f); //分母加1e-5f为了防止除0;
GGX模型中D项的计算代码为:
inline float GGXTerm (float NdotH, float roughness)
{
float a2 = roughness * roughness;
float d = (NdotH * a2 - NdotH) * NdotH + 1.0f; // 2 mad
return UNITY_INV_PI * a2 / (d * d + 1e-7f); // This function is not intended to be running on Mobile,
// therefore epsilon is smaller than what can be represented by half
}
迪斯尼列举了三种模型:
D
B
e
r
r
y
=
c
/
(
α
2
c
o
s
2
θ
h
+
s
i
n
2
θ
h
)
D
T
R
=
c
/
(
α
2
c
o
s
2
θ
h
+
s
i
n
2
θ
h
)
2
D
G
T
R
=
c
/
(
α
2
c
o
s
2
θ
h
+
s
i
n
2
θ
h
)
γ
c
=
α
2
/
π
中间的GGX模型实际上等价于TR(Trowbridge-Reitz)模型,第三项为Generalized-Trowbridge-Reitz模型,即指数项可变化;
可以看到,Unity采用的是中间GGX计算公式,不过在代码中有一些trick使用,为了在应用中达到更好的效果。
需要注意的是,unity的diffuseTerm项没有除以PI,而specularTerm项刚好又成了PI,所以可以看出,unity在内部的直接光照计算中对light强度进行了PI的缩放,即light.color = light.color * UNITY_PI;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。