渲染4 第一个光照
- 从物体空间到世界空间的法线变换
- 方向光的使用
- 漫反射和高光反射的计算
- Enforce energy conservation
- 使用金属工作流
- Unity的PBS算法的使用
这是渲染系列教程的第四篇教程。前一篇教程是关于合并贴图。本次教程我们来看看如何计算光照。
这个教程使用的是Unity 5.4.0,是当前最新的版本。
法线
我们可以看到物体,是因为我们的眼睛可以监测到电磁辐射。光的单个量子被称为光子。 我们的眼睛可以看到电磁波谱的一部分,这部分对我们来说即是可见光。其他的光谱我们是看不到的。
什么是整个电磁谱?
光谱被分为光谱带。 从低频到高频,这些被称为无线电波,微波,红外线,可见光,紫外线,X射线和γ射线
光源发光, 其中一些光线撞击物体, 一些这种光从物体反射。 如果那个光线最终碰到了我们的眼睛或者相机镜头 ,然后我们看到了对象。
要实现这些,我们必须知道物体的表面。 我们已经知道物体的位置,但并不知道其朝向。 为此,我们需要表面的法向量。
使用面片的法线
重复我们的第一个着色器,并使用它作为我们的第一个照明着色器。 使用此着色器创建材质并将其分配给场景中的某些立方体和球体。 给对象不同的旋转和缩放,一些不均匀,以获得多样的场景。
Unity使用包含了顶点法线的立方体和球体
动态批处理(Dynamic Batching)
这里有一个奇怪的现象发生在立方体的法线上。我们希望每个cube上面显示相同的颜色,但是实际的情况并不是这样的。更诡异的是这些立方体可以改变颜色,取决于我们看他们的角度。
这是因为动态批处理导致的。Unity会动态的把小的面片合并在一起,以减少Draw Call。球体的面片太大了(顶点数太多了,官方有对顶点数的限制做说明),所以不能动态合并,不起效果。但是立方体是合适的,可以进行动态合并的。
为了合并网格,它们必须从本地空间转换为世界空间。 物体是否以及如何批处理取决于如何对它们进行渲染排序。 由于这种转换也会影响法线,所以我们看到颜色发生变化
如果你想启用动态批处理,可以通过Player Setting进行切换。
除了动态批处理,Unity还可以做静态批处理。针对静态几何体的静态批处理工作方式不同于动态批处理,但是同样包含一个到世界空间的转换。静态批处理发生在build的时候。
尽管你需要意识到动态批处理,但是你无须担心。实际上,我们也要为我们的法线做相同的事情。所以你可以启用动态批处理。
世界空间的法线
除了动态批出理的物体,我们的法线都在物体自己的局部空间里。但是我们需要知道物体表面在世界空间中的朝向。因此我们需要将物体的法线从对象空间转换到世界空间。要实现这个转换,我们需要对象变换矩阵。
Unity将整个变换层次结构折叠成一个单一的变换矩阵,就像我们在第1部分中所做的那样。我们可以把这整个变换写成O=T1T2T3…T是一个单独的变换,而O是合并后的变换。这个矩阵就是著名的模型世界矩阵。
Unity使得这个矩阵可用,是通过一个float4x4类型的名为unity_ObjectToWorld的变量,这个变量定义在UnityShaderVariables中。在顶点shader中用法线乘上这个矩阵就可以把法线转换到世界空间中。因为法线是一个方向,所以它的坐标可以被忽略。因此齐次坐标的第4个分量为0。
Interpolators MyVertexProgram (VertexData v) {
Interpolators i;
i.position = mul(UNITY_MATRIX_MVP, v.position);
i.normal = mul(unity_ObjectToWorld, float4(v.normal, 0));
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
return i;
}
或者,我们可以只乘以一个3乘3的矩阵。编译后的代码最终是一样的。因为编译器会去掉所有与0相乘的部分。
i.normal = mul((float3x3)unity_ObjectToWorld, v.normal);
现在法线已经是世界空间中的法线了,但是有些物体比其他看起来更亮一些。那是因为他们的法线值被缩放了。因此在变换做完之后我们应该对其进行归一化。
i.normal = mul(unity_ObjectToWorld, float4(v.normal, 0));
i.normal = normalize(i.normal);
尽管我们做了归一化,但有些被缩放的物体看起来很奇怪,物体的法线没有一个统一的缩放。那是因为当一个表面在一个维度上进行了拉伸,它的法线并没有进行同样的拉伸。
我们应该对缩放进行转置。但旋转还是保持原样。我们应该怎么做呢?
我们描述物体的变换矩阵用O=T1T2T3。但是我们可以比这个用得更特殊一点。因为我们知道每一步其实是在对缩放,旋转,位移进行合并。因此我们可以把每一步拆解成SRP(Scale*Rotation*Position)。
这意思就是说O=S1R1P1S2R2P2S3R3P3…但是我们可以简写它。
因为法线只是一个方向,我们并不关心位置。因此我们可以简化成O=S1R1S2R2,并且我们只需要考虑3乘3的矩阵。
我们想对缩放矩阵进行求逆而保持旋转不变,于是我们想到使用一个新的矩阵N=S1-1R1S2-1R2
除了模型-世界矩阵,Unity还提供了世界-模型矩阵。这些矩阵的确可以互相转换。因此我们也可以写成
这种方式的确可以对缩放矩阵进行求逆,但是同样也对旋转矩阵进行了逆运算。幸运的是我们可以移出不想要的效果通过对矩阵进行转置。于是我们得到了
因此让我们用世界-模型矩阵的转置和法线相乘。
i.normal = mul(
transpose((float3x3)unity_WorldToObject),
v.normal
);
i.normal = normalize(i.normal);
事实上,UnityCG已经包含了一个方便的方法名为UnityObjectToWorldNormal,这个函数就做了这个事情
Interpolators MyVertexProgram (VertexData v) {
Interpolators i;
i.position = mul(UNITY_MATRIX_MVP, v.position);
i.normal = UnityObjectToWorldNormal(v.normal);
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
return i;
}
如果你想知道inline关键字是干嘛用的,那么我告诉你它什么事都没做。下面就是这个函数具体干了些什么事
// Transforms normal from object to world space
inline float3 UnityObjectToWorldNormal( in float3 norm ) {
// Multiply by transposed inverse matrix,
// actually using transpose() generates badly optimized code
return normalize(
unity_WorldToObject[0].xyz * norm.x +
unity_WorldToObject[1].xyz * norm.y +
unity_WorldToObject[2].xyz * norm.z
);
}
Renormalizing
漫反射
我们看到自己不是光源的物体,因为它们反射光。有不同的方式可以发生这种反射。我们先考虑漫反射。
发生漫射反射,因为光线不仅仅是从表面反弹。相反,它穿透表面,反弹一点,分裂几次,直到它再次离开表面。在现实中,光子和原子之间的相互作用比这更复杂,但我们不需要在这个细节上了解真实世界的物理学。
从表面漫反射多少光取决于光线撞击它的角度。当表面以0°角度正面朝上时,大部分光线被反射。随着角度的增加,反射将减小。在90°时,没有光再次撞到表面,所以它保持黑暗。散射光的量与光方向和表面法线之间的角度的余弦成正比。这被称为兰伯特余弦定律。
我们可以通过计算表面法线和光方向的点积来确定这个兰伯特反射系数。 我们已经知道了法线,但还没有光线方向。 我们从一个固定的光线方向开始,直接来自于上方的光。
对光照进行夹值
当表面正面朝向光源,计算点乘工作正常,但远离光的方向就不正确了。那种情况下,表面从逻辑层面上应该处于它们的阴影当中,它们按道理一点光都接收不到。因为光的方向和法线的方向之间的角度在这个点处大于了90度,所以它的余弦值点乘的结果是负数。由于我们不想要一个负数的光,所以我们必须对这个值进行夹值。我们可以用标准的 max函数来做这件事情。
return max(0, dot(float3(0, 1, 0), i.normal));
替代 max 函数,你应该经常看到shader 里面用到的是saturate。这是标准的夹值函数,夹值的区间是(0,1)。
return saturate(dot(float3(0, 1, 0), i.normal));
这个看起来没得必要,因为我们都知道我们点乘的结果绝对不会超过1。然而,在某些情况下它更有效,依赖于硬件。但这个时候我们不需要担心像这种微小的优化。事实上我们可以把这个委托给 Unity来做。
UnityStandardBRDF*引入文件定义了一个方便的函数**DotClamped。这个函数起到一个点乘的作用并且保证值不为负数。这正是我们想要的。它同样也包含了其他光照函数,也引入了其他有用的文件,我们后续会需要他们。那我们就用起来吧。
#include "UnityCG.cginc"
#include "UnityStandardBRDF.cginc"
…
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
i.normal = normalize(i.normal);
return DotClamped(float3(0, 1, 0), i.normal);
}
因为UnityStandardBRDF引进引入了UnityCG和一些其他文件,所以我们不必显示的引入了。如果引入了也不会出错,但是我们最好是简化。
//#include "UnityCG.cginc"
#include "UnityStandardBRDF.cginc"
光源
为了不使用硬编码的光源方向,我们应该使用我们场景中的光的真实方向。默认,每个Unity的场景都有一个光,代表太阳光。它是方向光,意味着可以认为它无线远。因此,所有的光线的方向都来至于同一个方向。当然在现实生活中不可能是真的,但太阳光足够的远,基本也近似于是一个平行光了。
UnityShaderVariables定义了一个float4的变量_WorldSpaceLightPos0,这个变量包含了当前光源的位置。如果是方向光的话,这个值代表方向光的方向(因为方向光位置没有意义)。这个值包含4个分量,因为这些是齐次坐标。因此第4个分量为0。
float3 lightDir = _WorldSpaceLightPos0.xyz;
return DotClamped(lightDir, i.normal);
光照模式
在产生一个正确的光照结果之前,我们必须先告诉Unity 哪个光照数据我们想去使用。我们靠在我们的 shader 的 pass 中添加LightMode 来完成这件事情。
我们采用哪个光照模式取决于我们想如何的去渲染我们的场景。我们既可以选择前向渲染路径 forward,也可以选择延时渲染路径deferred。这里还有2个更老的渲染模式,但我们不讨论它们。你通过 player settings 来选择渲染路径。我们采用前向渲染路径,它也是默认的。
我们必须使用ForwardBase 通道(pass)。这是我们通过使用前向渲染路径来渲染一些东西的时候,第1个采用的pass。它让我们能够访问场景中的主要的方向光。它也会对其他进行设置,稍后我们会涉及到。
Pass {
Tags {
"LightMode" = "ForwardBase"
}
CGPROGRAM
…
ENDCG
}
灯光颜色
当然光的颜色不会总是白色的。每个光源都有它自己的颜色。而这个颜色我们可以通过一个名为_LightColor0 类型为float4的变量来定义,这个变量被定义在UnityLightingCommon中。
低精度的数值,用于移动设备用精度来交换速度。桌面程序中fixed4是float4的一个别名而已。精度优化是后续章节要涉及到的一个主题。
这个变量包含了光的颜色,被乘上了它的强度。尽管它支持了4个通道,但是我们仅仅只需要使用RGB三个通道。
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
i.normal = normalize(i.normal);
float3 lightDir = _WorldSpaceLightPos0.xyz;
float3 lightColor = _LightColor0.rgb;
float3 diffuse = lightColor * DotClamped(lightDir, i.normal);
return float4(diffuse, 1);
}
Albedo
Albedo与Diffuse的区别?
https://computergraphics.stackexchange.com/questions/350/albedo-vs-diffuse
镜面反射
除了漫反射,还有高光反射。当光线碰到物体表面时。相反,光线从表面反弹,并且角度等于打到表面上的角度。这就是你看镜子所发生的反射的原因。
与漫反射不同,观察者的位置对于镜面反射很重要。只有最后才能直接反射的光线才可见。其余的会在别的地方,所以你不会看到它。
所以我们需要知道从表面到观察者的方向。这需要表面和相机的世界空间位置。
我们可以通过对象到世界矩阵来确定顶点程序中表面的世界位置,然后将其传递给片段程序。
struct Interpolators {
float4 position : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
Interpolators MyVertexProgram (VertexData v) {
Interpolators i;
i.position = mul(UNITY_MATRIX_MVP, v.position);
i.worldPos = mul(unity_ObjectToWorld, v.position);
i.normal = UnityObjectToWorldNormal(v.normal);
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
return i;
}
相机的位置可以通过访问float3类型的变量_WorldSpaceCameraPos来得到,这个变量定义在UnityShaderVariables里面。我们可以得到这个视图方向,通过【相机的位置-物体表面的位置】,然后归一化。
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
i.normal = normalize(i.normal);
float3 lightDir = _WorldSpaceLightPos0.xyz;
float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
float3 lightColor = _LightColor0.rgb;
float3 albedo = tex2D(_MainTex, i.uv).rgb * _Tint.rgb;
float3 diffuse =
albedo * lightColor * DotClamped(lightDir, i.normal);
return float4(diffuse, 1);
}
反射光
要想知道哪里是反射光执行的地方,答案是reflect标准函数。它拿到入射光线的方向,然后基于物体的表面的法线进行反射。因此这里我们就不能再用我们的光的方向了。
float3 reflectionDir = reflect(-lightDir, i.normal);
return float4(reflectionDir * 0.5 + 0.5, 1);
平滑
高光效果影响范围的大小,取决于材质的粗糙程度。平滑的材质对光的聚集比较好,因此高光的范围要小些。我们可以通过材质球的属性来控制这个平滑。典型的就是定义一个介于0到1之间的值。
Properties {
_Tint ("Tint", Color) = (1, 1, 1, 1)
_MainTex ("Texture", 2D) = "white" {}
_Smoothness ("Smoothness", Range(0, 1)) = 0.5
}
…
float _Smoothness;
Blinn-Phong
我们目前使用的计算反射的,是根据Blinn(布林)反射模型。但是通常使用的是Blinn-Phong反射模型。Blinn-Phong反射模型使用了光照方向和视线方向之间的一个半角向量。法线与这个半角向量的点乘乘积决定了高光贡献了多少亮度。
镜面颜色
漫反射和镜面反射
能量守恒
仅仅将漫反射和高光反射加在一起,这里会出问题。结果会变得比光源还亮。尤其当我们使用一个完全白色的高光,混合使用一个低光滑度的时候特别明显。
当光撞击表面,部分光反弹成为高光。剩余的部分要么穿透表面,要么返回回来成为漫反射光,或者被吸收。但是我们目前并没有考虑到这个。相反,我们的光既以一个满强度进行反射又以一个满强度进行散射。因此我们最终会得到2倍强度的光照能量。
我们必须确保材质的散射部分和高光部分的光能总和不能超过1。那样就保证了我们不是在创建光。如果总和小于1,效果还好。那意味着部分光被吸收了。
因为我们使用的
金属的工作流
金属的工作流程
我们通常关心的最基本的有两种材质,一种是金属,一种是非金属。我们都知道后者(非金属)是绝缘体材质。当前,我们可以通过使用一个强的高光来创建金属。而对于绝缘体材质的创建,我们可以使用一个微弱的单色高光。这是高光流程。
如果我们能在金属和非金属之间切换就简单多了。由于金属没有漫反射,我们可以使用这些颜色数据来代替它们的高光。而非金属也没有彩色的高光,所以我们不需要单独的高光。这就是所谓的金属工作流程。让我们用这个流程来试试看。
两种方法都不错。这就是为什么Unity有一个标准的着色器。金属工作流程更简单,因为你只有一个颜色源外加上一个滑块。这足以创造逼真的材质。高光工作流程可以产生相同的结果,但是由于您有更多的控制,因此也有可能产生不真实的材质。
我们可以使用另一个滑块属性作为金属切换,以取代高光色调。通常,它应该被设置为0或1,因为某些东西是金属或不是金属。其间的值表示具有金属和非金属组分的混合物的材质。
Properties {
_Tint ("Tint", Color) = (1, 1, 1, 1)
_MainTex ("Albedo", 2D) = "white" {}
// _SpecularTint ("Specular", Color) = (0.5, 0.5, 0.5)
_Metallic ("Metallic", Range(0, 1)) = 0
_Smoothness ("Smoothness", Range(0, 1)) = 0.1
}
…
// float4 _SpecularTint;
float _Metallic;
float _Smoothness;
现在我们可以从漫反射和金属属性来推导出高光颜色。漫反射可以简单的乘以一减去金属率值。
float3 specularTint = albedo * _Metallic;
float oneMinusReflectivity = 1 - _Metallic;
//albedo =EnergyConservationBetweenDiffuseAndSpecular(
// albedo, _SpecularTint.rgb,oneMinusReflectivity</del>
// );
albedo *= oneMinusReflectivity;
float3 diffuse = albedo * lightColor * DotClamped(lightDir, i.normal);
float3 halfVector = normalize(lightDir + viewDir);
float3 specular = specularTint * lightColor * pow(DotClamped(halfVector, i.normal),
);
但是,这是一个过于简单的过程。即使是纯绝缘体仍然有一些高光反射。因此,高光强度和反射值与金属率的值并不完全匹配。这也受到色彩空间的影响。幸运的是,UnityStandardUtils也具有DiffuseAndSpecularFromMetallic这一函数,为我们解决了这个问题。
float3 specularTint;
// = albedo * _Metallic;
float oneMinusReflectivity; // = 1 - _Metallic;
//albedo *= oneMinusReflectivity;
albedo = DiffuseAndSpecularFromMetallic(albedo, _Metallic, specularTint, oneMinusReflectivity
);
注意这里使用的unity_ColorSpaceDielectricSpec是half4类型的变量,这个变量是Unity设置的,基于颜色空间。
inline half OneMinusReflectivityFromMetallic(half metallic) {
// We'll need oneMinusReflectivity, so
// 1-reflectivity = 1-lerp(dielectricSpec, 1, metallic)
// = lerp(1-dielectricSpec, 0, metallic)
// store (1-dielectricSpec) in unity_ColorSpaceDielectricSpec.a, then
// 1-reflectivity = lerp(alpha, 0, metallic)
// = alpha + metallic*(0 - alpha)
// = alpha - metallic * alpha
half oneMinusDielectricSpec = unity_ColorSpaceDielectricSpec.a;
return oneMinusDielectricSpec - metallic * oneMinusDielectricSpec;
}
inline half3 DiffuseAndSpecularFromMetallic (
half3 albedo, half metallic,
out half3 specColor, out half oneMinusReflectivity
) {
specColor = lerp(unity_ColorSpaceDielectricSpec.rgb, albedo, metallic);
oneMinusReflectivity = OneMinusReflectivityFromMetallic(metallic);
return albedo * oneMinusReflectivity;
}
其中有一个细节是金属率本身应该在伽马空间。但是,当在线性空间中渲染时,单个值不会被Unity自动进行伽马校正。我们可以使用该Gamma属性告诉Unity,也应该对我们的金属率进行伽马校正。
[Gamma] _Metallic ("Metallic", Range(0, 1)) = 0
不幸的是,到目前为止,非金属的高光反射现在变得相当模糊。为了改善这一点,我们需要更好的计算光照的方法。
基于物理的着色
Blinn-Phong算法长期以来一直是游戏行业的主流算法。但是如今基于物理的渲染,众所周知的PBS已经如火如荼了。基于物理的渲染之所以好,是因为它好在更真实和更加可预测。理想情况下,游戏引擎和建模工具都使用相同的着色算法。这使得内容的创建更容易,也就是建模时的效果拿到游戏引擎里面渲染出来的效果就是最开始设计想要的那样。游戏行业已经慢慢的在实时标准的PBS算法了。
Unity的标准着色器也使用PBS方法。Unity实际上有多个实现。它根据目标平台,硬件和API级别决定使用哪一种。该算法可以通过UNITY_BRDF_PBS在UnityPBSLighting中定义的宏来访问。BRDF代表双向反射分布函数。
// #include“UnityStandardBRDF.cginc”
// #include“UnityStandardUtils.cginc”
#include“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
#define UNITY_BRDF_PBS BRDF3_Unity_PBS
#elif UNITY_PBS_USE_BRDF3
#define UNITY_BRDF_PBS BRDF3_Unity_PBS
#elif UNITY_PBS_USE_BRDF2
#define UNITY_BRDF_PBS BRDF2_Unity_PBS
#elif UNITY_PBS_USE_BRDF1
#define UNITY_BRDF_PBS BRDF1_Unity_PBS
#elif defined(SHADER_TARGET_SURFACE_ANALYSIS)
// we do preprocess pass during shader analysis and we dont
// actually care about brdf as we need only inputs/outputs
#define UNITY_BRDF_PBS BRDF1_Unity_PBS
#else
#error something broke in auto-choosing BRDF
#endif
\#endif
这些函数是相当数学化的东西,所以我不会深入介绍这些细节。它们同样会计算漫反射和高光反射,只是以一种不同于 Blinn-Phong的方式进行计算。此外,还有一个菲涅尔反射组件。这个菲涅尔反射增加了
为了确保Unity选择最好的BRDF功能,我们必须至少定位着色器级3.0。我们用一个pragma语句来做到这一点。
CGPROGRAM
#pragma target 3.0
#pragma vertex MyVertexProgram
#pragma fragment MyFragmentProgram
Unity的BRDF函数返回RGBA颜色,alpha分量始终设置为1.因此,我们可以直接使我们的片段程序返回其结果。
// float3 diffuse =
// albedo * lightColor * DotClamped(lightDir, i.normal);
// float3 halfVector = normalize(lightDir + viewDir);
// float3 specular = specularTint * lightColor * pow(
// DotClamped(halfVector, i.normal),
// _Smoothness * 100
// );
return UNITY_BRDF_PBS();
我们当然必须用参数来调用它。这些函数有八个参数。前两个是材质球的漫反射和高光反射颜色。我们已经有了。
return UNITY_BRDF_PBS(
albedo, specularTint
);
接下来的两个参数必须是反射率和粗糙度。这些参数必须是一个减号的形式,这是一个优化。
return UNITY_BRDF_PBS(
albedo, specularTint,
oneMinusReflectivity, _Smoothness
);
当然表面法线和视线方向也是需要的。它们成为第5个和第6个参数。
return UNITY_BRDF_PBS(
albedo, specularTint,
oneMinusReflectivity, _Smoothness,
i.normal, viewDir
);
最后2个参数必须是直接光和间接光。
光照结构体
UnityLightingCommon定义了一个简单的结构体UnityLight,这个结构体是用于 Unity 中的 shader传递光照数据的。它包含了光的颜色,方向和一个叫ndot1的值,这是漫反射这一项。记住,这些结构体单纯是为了我们使用方便,它不影响 shader 代码的编译。
我们拥有这所有的信息,因此我们只需要把它放在一个光照结构中去,并作为第7个参数传递进去。
UnityLight light;
light.color = lightColor;
light.dir = lightDir;
light.ndotl = DotClamped(i.normal, lightDir);
return UNITY_BRDF_PBS(
albedo, specularTint,
oneMinusReflectivity, _Smoothness,
i.normal, viewDir,
light
);
既然BRDF函数里面的数据全部需要自己计算,那为什么我们必须提供漫反射这一项呢?情况就是这样,因为光结构也用于其他情况。
实际上,GGX BRDF版本甚至不使用ndotl。它自己计算。与往常一样,着色器编译器将去除所有未使用的代码。所以你不用担心。
最后的参数是间接光。我们必须使用UnityLightingCommon中UnityIndirect定义的结构。它包含两种颜色,一种是漫反射的。漫反射颜色代表环境光,而高光颜色代表环境反射。
我们稍后会介绍间接光,所以现在只需将这些颜色设置为黑色。
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
i.normal = normalize(i.normal);
float3 lightDir = _WorldSpaceLightPos0.xyz;
float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
float3 lightColor = _LightColor0.rgb;
float3 albedo = tex2D(_MainTex, i.uv).rgb * _Tint.rgb;
float3 specularTint;
float oneMinusReflectivity;
albedo = DiffuseAndSpecularFromMetallic(
albedo, _Metallic, specularTint, oneMinusReflectivity
);
UnityLight light;
light.color = lightColor;
light.dir = lightDir;
light.ndotl = DotClamped(i.normal, lightDir);
UnityIndirect indirectLight;
indirectLight.diffuse = 0;
indirectLight.specular = 0;
return UNITY_BRDF_PBS(
albedo, specularTint,
oneMinusReflectivity, _Smoothness,
i.normal, viewDir,
light, indirectLight
);
}