目录
[TOC]
写在前面
本文将简述Blinn-Phong光照模型以及PBS光照模型在UnityShader中的使用。本文只涉及单一光源的光照处理,多光源光照处理请见下一篇-初级–多光源光照。全篇基于Unity2018.3.2。
在PC前的各位读者,建议下载MD文件阅读,观感更好。若有图片无法显示,请开启网络。欢迎转载,只需注明转载来源地址即可。MD文件及工程Package下载地址(提取码:4c1s)
Unity中的Workflow
1.Metallic Workflow(金属工作流)
有”Metallic”值,表示材质是否为金属性。在使用金属性材质的情况下,反照率颜色(Albedo)将控制镜面反射的颜色,且大多数光线以镜面反射形势反射。非金属性材质将具有与入射光颜色相同的镜面反射,并且在正面观察表面时几乎不会反射。
2.SpecularWorkflow(镜面反射工作流)
有镜面反射颜色(Specular),用于控制材质的镜面反射颜色,镜面反射颜色可与漫反射颜色不同。
Blinn-Phong光照模型
反射(diffuse)
说明:符合兰伯特定律(Lambert’s law)反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比
计算公式: 其中:
半兰伯特光照模型(无物理依据,仅视觉加强):增强物体背面暗处的光照效果,将余弦值的[-1,1]映射到[0,1]。
计算公式:
其中:
高光反射(specular)
说明:经验模型,需要信息: 其中反射方向的计算:
方法一:(Phong) 方法二:(Blinn)使用新矢量h(光源方向和视角方向的半角向量)代替反射方向矢量。当摄像机与光源距离模型足够远的时候,此方法计算更快。 计算公式:
(Phong) (Blinn) 其中:
实现步骤:
1.创建文件夹结构:
使用HTUtility工具(Shift+Alt+G)创建合适的文件夹结构,重命名Root文件夹为”单一光源光照”。在场景中创建胶囊体”Capsule”,Reset到原点位置。创建Material,命名为”SingleLightingMat”;创建Unlit Shader,命名为”SingleLighting”。选择”SingleLightingMat”的Shader为”SingleLighting”,并将材质赋值给场景中的胶囊体。
2.设置环境光:
在Unity菜单中点击“Window–Rendering–LightingSettings”。设置天空盒子和环境光(Environment Lighting)。
3.Shader编写:
先删除所有和雾相关的代码,在这里我们不需要雾。
属性块:
Properties
{
_Tint("Tint",COLOR) = (1,1,1,1)//色调
_MainTex ("Albedo", 2D) = "white" {}//反照率,diffuse颜色
_Smoothness("Smoothness",Range(0,1)) = 0.5//物体表面平滑程度,越平滑越容易聚焦光线、光点越小
_SpecularTint("Specular",Color) = (0.5,0.5,0.5,1)//镜面反射颜色
}
对应的在Pass中的属性:
fixed4 _Tint;
sampler2D _MainTex;
float4 _MainTex_ST;
float _Smoothness;//物体表面平滑程度,越平滑越容易聚焦光线,光点越小
float4 _SpecularTint;//镜面反射颜色
由于本例只处理单一光源,所以我们只需要一个Pass,为其设置前向渲染的标签:
Pass
{
Tags{"LightMode"="ForwardBase"}//前向渲染
......
}
由于本例中需要用到Unity内置的DotClamped
函数和EnergyConservationBetweenDiffuseAndSpecular
能量守恒函数,所以需要分别引入”UnityStandardBRDF.cginc“库和UnityStandardUtils.cginc库。
库文件最好不要重复引入,虽然大多数情况下Unity会自动剔除重复引入的库,但重复引入库会导致变量重复命名的编译错误。库文件的层次结构:
在Pass块中的CGPROGRAM和ENDCG中引入库文件:
Pass
{
Tags{"LightMode"="ForwardBase"}//前向渲染
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//定义了DotClamped,计算兰伯特的cos值--Clamp在[0,1]
#include "UnityStandardBRDF.cginc"
//已经包含UnityCG.cginc了
//定义了能量守恒函数:EnergyConservationBetweenDiffuseAndSpecular
#include "UnityStandardUtils.cginc"
......
ENDCG
}
顶点着色器的输入结构体:
struct appdata
{
float4 pos : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
顶点着色器的输出结构体/片元着色器的输入结构体:
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
在顶点函数中需要做的事情:
1.将顶点坐标从模型空间转换到裁剪空间,可以直接使用Unity自带API:o.vertex = UnityObjectToClipPos(v.pos);
。
2.处理UV坐标:o.uv = TRANSFORM_TEX(v.uv, _MainTex);
。
3.获取世界空间下的法线:o.normal = UnityObjectToWorldNormal( v.normal);
。通过此API获取的世界空间下的法线向量将不再是单位向量,会比单位向量小一点点。
4.获取世界空间下的顶点坐标:o.worldPos = mul(unity_ObjectToWorld , v.pos).xyz;
。
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.pos);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.normal = UnityObjectToWorldNormal( v.normal);
o.worldPos = mul(unity_ObjectToWorld , v.pos).xyz;
return o;
}
在片元函数中要做的事情:
1.计算漫反射颜色(diffuse)。
需要光源颜色:fixed3 lightColor = _LightColor0.rgb;//该Pass处理的逐像素光源的颜色
反照率[^Albedo?]:先对主纹理贴图采样,顺带乘以颜色基调:float3 albedo = tex2D(_MainTex,i.uv).rgb * _Tint.rgb;
;然后通过能量守恒公式调整反照率[^为什么需要使用能量守恒公式?]:albedo = EnergyConservationBetweenDiffuseAndSpecular(albedo , _SpecularTint.rgb , oneMinusReflectivity);
;最后使用DotClamped计算光源方向向量和法线单位向量的点积:float3 diffuse = lightColor * albedo * DotClamped(lightDir,i.normal);
。
其中,在UnityShader库中的CGIncludes文件夹中可以搜索Unity自带函数:
- (1).函数”EnergyConservationBetweenDiffuseAndSpecular“:
// Diffuse/Spec Energy conservation 能量守恒
inline half3 EnergyConservationBetweenDiffuseAndSpecular (half3 albedo, half3 specColor, out half oneMinusReflectivity)
{
oneMinusReflectivity = 1 - SpecularStrength(specColor);
#if !UNITY_CONSERVE_ENERGY
return albedo;
#elif UNITY_CONSERVE_ENERGY_MONOCHROME
return albedo * oneMinusReflectivity;
#else
return albedo * (half3(1,1,1) - specColor);
#endif
}
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;
}
- (2).函数”DotClamped“用于计算点积后再保证其为正值:
inline half DotClamped (half3 a, half3 b)
{
#if (SHADER_TARGET < 30)
return saturate(dot(a, b));
#else
return max(0.0h, dot(a, b));
#endif
}
2.计算镜面反射颜色(specular)。
可以使用Phong模型或者Blinn模型计算。本文使用的Blinn模型。需要计算光源方向向量和视角方向向量之间的半角向量:float3 halfDir = normalize( lightDir + viewDir );
;使用Blinn模型计算镜面反射颜色:float3 specular = lightColor * _SpecularTint.rgb * pow( DotClamped( i.normal , halfDir ) , _Smoothness * 100 );
。
3.计算法线单位向量的时候:法线在通过插值器传递的时候,会变短一点点,需要再次归一化。移动端可牺牲此步以提升性能。(常见优化手段)
fixed4 frag (v2f i) : SV_Target
{
fixed3 lightColor = _LightColor0.rgb;//该Pass处理的逐像素光源的颜色
//法线在通过插值器传递的时候,会变短一点点,需要再次归一化
//移动端 可牺牲此步以提升性能。(常见优化手段)
i.normal = normalize(i.normal);
float3 lightDir = _WorldSpaceLightPos0.xyz;
float3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
float3 halfDir = normalize( lightDir + viewDir );
//物体的纹理颜色即为反照率
float3 albedo = tex2D(_MainTex,i.uv).rgb * _Tint.rgb;
//能量守恒:此步保证了漫反射加镜面反射小于1,即不会凭空制造光
//albedo *= 1 - max(_SpecularTint.r , max(_SpecularTint.g , _SpecularTint.b));
//1-反射率=1 - max(_SpecularTint.r , max(_SpecularTint.g , _SpecularTint.b))
float oneMinusReflectivity;
//能量守恒,会out出oneMinusReflectivity供其他地方使用
albedo = EnergyConservationBetweenDiffuseAndSpecular(albedo , _SpecularTint.rgb , oneMinusReflectivity);
//兰伯特:
float3 diffuse = lightColor * albedo * DotClamped(lightDir,i.normal);
//半兰伯特:
//float3 diffuse = lightColor * albedo * (dot(lightDir,i.normal) * 0.5 + 0.5);
float3 specular = lightColor * _SpecularTint.rgb * pow( DotClamped( i.normal , halfDir ) , _Smoothness * 100 );
return float4( diffuse + specular , 1 );
}
导入Albedo纹理:
将Albedo赋值给Shader后,调整直接光方向到合适方向后,效果如下:
可见物体背面较黑,可用半兰伯特光照模型进行改善,改善后效果如下:
PBS(Physically-Based Shading)
本文不对PBS的数学部分进行解释,读者如果有兴趣可自行查阅相关资料。
实现步骤:
1.创建文件夹结构:
方法同上,创建胶囊体、Material、Shader
2.Shader编写:
属性块:
添加的金属值需要在属性前面加上[Gamma]
,告诉Unity该属性值也需要进行Gamma校正,以防在线性空间绘制时金属值不被Gamma校正。
Properties
{
_Tint("Tint",COLOR) = (1,1,1,1)
_MainTex ("Albedo", 2D) = "white" {}
//告诉Unity,金属值也需要进行Gamma校正
[Gamma]_Metallic("Metallic",Range(0,1)) = 0//金属值
_Smoothness("Smoothness",Range(0,1)) = 0.1//物体表面平滑程度,越平滑越容易聚焦光线、光点越小
}
对应的在Pass中的属性:
fixed4 _Tint;
sampler2D _MainTex;
float4 _MainTex_ST;
float _Smoothness;//物体表面平滑程度,越平滑越容易聚焦光线,光点越小
float _Metallic;//金属值
同样此Shader也只有一个Pass通道:Tags{"LightMode"="ForwardBase"}//前向渲染
。
要使用PBS必须指定目标平台在3.0及以上:#pragma target 3.0
。
引入库:#include "UnityPBSLighting.cginc"
,可以不再引入上一个Shader引入的那两个库了,因为此库已经包含有。
Pass
{
Tags{"LightMode"="ForwardBase"}//前向渲染
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//使用PBS
#pragma target 3.0
//包含"UnityStandardBRDF.cginc"和"UnityStandardUtils.cginc"库
#include "UnityPBSLighting.cginc"
......
ENDCG
}
顶点着色器的输入结构体:
struct appdata
{
float4 pos : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
顶点着色器的输出结构体/片元着色器的输入结构体:
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
在顶点函数中需要做的事情:
1.将顶点坐标从模型空间转换到裁剪空间,可以直接使用Unity自带API:o.vertex = UnityObjectToClipPos(v.pos);
。
2.处理UV坐标:o.uv = TRANSFORM_TEX(v.uv, _MainTex);
。
3.获取世界空间下的法线:o.normal = UnityObjectToWorldNormal( v.normal);
。通过此API获取的世界空间下的法线向量将不再是单位向量,会比单位向量小一点点。
4.获取世界空间下的顶点坐标:o.worldPos = mul(unity_ObjectToWorld , v.pos).xyz;
。
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.pos);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.normal = UnityObjectToWorldNormal( v.normal);
o.worldPos = mul(unity_ObjectToWorld , v.pos).xyz;
return o;
}
在片元函数中要做的事情:
1.通过金属值计算反照率和镜面反射颜色:albedo = DiffuseAndSpecularFromMetallic(albedo , _Metallic , specColor , oneMinusReflectivity);
;其中”oneMinusReflectivity“为”1-反射率“,反射率为镜面反射颜色RGB通道中的最大值。
其中,在UnityShader库中的CGIncludes文件夹中可以搜索Unity自带函数:
- (1).函数”DiffuseAndSpecularFromMetallic“:会返回处理后的Albedo和oneMinusReflectivity值。
通过albedo * oneMinusReflectivity;
可以保证能量不会增加。(数学推导比较简单,此处略)
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;
}
- (2).(1)中函数中的函数”OneMinusReflectivityFromMetallic“:
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;
}
2.使用Unity自带的双向反射分布函数UNITY_BRDF_PBS(...)
计算最后的光照颜色:
//PBS需要使用的光数据
UnityLight light;
light.color = lightColor;
light.dir = lightDir;
light.ndotl = DotClamped(i.normal , lightDir);
//PBS需要使用的间接光数据
UnityIndirect indirectLight;
indirectLight.diffuse = 0;//漫反射代表环境光
indirectLight.specular = 0;//镜面反射代表环境反射
return UNITY_BRDF_PBS
(
albedo , specColor ,
oneMinusReflectivity ,_Smoothness ,
i.normal ,viewDir ,
light , indirectLight
);
片元函数:
fixed4 frag (v2f i) : SV_Target
{
fixed3 lightColor = _LightColor0.rgb;//该Pass处理的逐像素光源的颜色
//法线在通过插值器传递的时候,会变短一点点,需要再次归一化
//移动端 可牺牲此步以提升性能。(常见优化手段)
i.normal = normalize(i.normal);
float3 lightDir = _WorldSpaceLightPos0.xyz;
float3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
//物体的纹理颜色即为反照率
float3 albedo = tex2D(_MainTex,i.uv).rgb * _Tint.rgb;
//1-反射率=1 - max(_SpecularTint.r , max(_SpecularTint.g , _SpecularTint.b))
float oneMinusReflectivity;
float3 specColor;
//通过金属值计算反照率和镜面反射颜色
//返回albedo,out specColor和oneMinusReflectivity
albedo = DiffuseAndSpecularFromMetallic(albedo , _Metallic , specColor , oneMinusReflectivity);
//PBS需要使用的光数据
UnityLight light;
light.color = lightColor;
light.dir = lightDir;
light.ndotl = DotClamped(i.normal , lightDir);
//PBS需要使用的间接光数据
UnityIndirect indirectLight;
indirectLight.diffuse = 0;//漫反射代表环境光
indirectLight.specular = 0;//镜面反射代表环境反射
return UNITY_BRDF_PBS
(
albedo , specColor ,
oneMinusReflectivity ,_Smoothness ,
i.normal ,viewDir ,
light , indirectLight
);
}
回到Unity中,调整直接光方向到合适方向后,效果如下:
(左边为SpecularWorkflow;右边为MetallicWorkflow)
其中”SpecularWorkflow”的设置:
“MetallicWorkflow”的设置:
写在最后
本文着重讲述了两种工作流在Unity中的简单实现,本文只处理了单一光源,对于多光源和阴影等支持将在后续文章中给出。
Reference
UnityShader入门精要