声明:本文的内容都是对Games101课程和《Unity Shader入门精要》的标准光照模型部分的总结,图片均取自闫老师的课件。
标准光照模型
标准光照模型是经验模型,只关注直接光照,即那些直接从光源发射出来照射到物体表面后,经过物体表面的一次反射直接进入摄像机的光线。
标准光照模型将进入摄像机的光线分为4个部分:自发光、环境光、漫反射和高光反射。
自发光
自发光是由物体发出的光直接进入摄像机,自发光的表面并不会照亮周围的表面,因此这个物体不会被当成一个光源。。由于大多数物体并没有自发光属性,因此在编写Unity Shader时,并不计算自发光部分,若需要计算自发光,可在片元着色器输出最后的颜色之前把材质的自发光颜色添加到输出颜色上。
环境光
环境光用于模拟间接光照,即从其他物体反射到该物体的光,再反射到摄像机。环境光和光线的入射角度以及观察的角度无关,在任何方向上看上去都是一样的。因此在Unity Shader编写中,环境光通常是一个全局变量,场景中的所有物体都使用这个环境光。环境光的计算公式如下
\( La=ka*Ia \)
ka是环境光系数,Ia是环境光,La是反射出的环境光。
Unity Shader code如下:
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
漫反射
漫反射光照是被物体表面随机散射到各个方向的,即漫反射没有方向性,反射是完全随机的。因此漫反射和光线的入射角度有关,和反射无关。在标准光照模型中,漫反射部分可以概况为3个问题,即光线到达物体表面的能量是多少?光线入射方向和表面法线的角度是多少?物体表面的漫反射系数是多少?
光线到达物体表面的能量是多少?
如图,当光源向四周发射光时,我们可以认为在单位球上的能量为I。那么当光线到达远处的一点时,到达该点的能量则为I/r^2.
光线入射方向和表面法线的角度是多少?
为什么要知道光线入射方向和表面法线的角度?如下图可以看到。光线到达物体表面后,反射光线的强度和表面法线与光线方向的夹角余弦有关。
物体表面的漫反射系数是多少?
最后,我们要知道每个物体的漫反射系数。即材质的漫反射颜色。
最终的漫反射计算如下:
Unity Shader Code:
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLightDir));
高光反射
高光反射,即沿着完全镜面反射方向被反射的光线。在日常生活中,我们可以看到物体表面的高光和观察的方向是有关系的。观察方向和光线的反射方向越接近,高光越明显。如下图所示:
因此高光反射的计算,我们要知道光线的入射角度、光线的出射角度、光线到达物体表面的颜色和强度、物体表面的高光系数(即材质的高光反射颜色)和光泽度。
Phong模型的计算方式如下:
$$Ls=ks*(I/r^2)*max(0,R*v)^p$$
p是光泽度,它控制了高光的区域大小,如下图所示,p越大,高光面积越小
Unity Shader Code:
fixed3 reflectDir = normalize(reflect(-worldLightDir,worldNormal));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld,v.vertex).xyz);
fixed3 specular = _LightColor0.rgb * _Speculart.rgb * pow(saturate(dot(reflectDir,viewDir)),_Gloss);
Phong模型要计算光线的反射方向,Blinn提出了一个简单的修改方法来得到类似的效果。Blinn发现反射方向和观察方向有多近,那么光源方向和观察方向的半程向量和表面法线就有多近。因此,可以避免计算反射方向R,而是计算光源方向和观察方向的半程向量:
半程向量非常好算,其就是将光源方向和观察方向相加再归一化。
Blinn Phong模型高光部分的Unity Shader Code:
fixed3 halfDir = normalize(viewDir + worldLightDir);
fixed3 specular = _LightColor0.rgb * _Speculart.rgb * pow(saturate(dot(halfDir,worldNormal)),_Gloss);
最终的Blinn Phong模型如下图:
着色频率
至此,我们已经知道了基本光照模型的计算公式。但是我们应该在哪里计算这些光照模型呢?
通常,我们有三种选择:
- 计算一次光照,然后在整个三角形上使用。(Flat shading)
- 计算三角形每个顶点的光照,然后对三角形内部做插值,顶点法线可由顶点周围的三角形的法线加权平均获得。(Gouraud shading)
- 对每一个像素计算它的光照,这需要对顶点法线做插值,计算每个像素上的法线。(Phong shading)
如下图所示,逐像素光照在每个像素上计算光照,因此它的效果最好,但是它要在每个像素上计算光照,因此计算量最大。但是当模型的三角形足够多的时候,三种着色方法效果会趋于一致。甚至当三角形非常多的时候,三角形数量大于像素数量时,Flat shading的计算量比Phong shading的计算量要大。
Unity Shader Code和结果图
逐顶点光照code:
Shader "Unlit/SpecularVertexShader"
{
Properties
{
_Diffuse ("Diffuse", Color) = (1,1,1,1)
_Speculart ("Specular",Color) = (1,1,1,1)
_Gloss ("Gloss", Range(8.0,256)) = 20
}
SubShader
{
Pass
{
Tags { "LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
// #pragma multi_compile_fog
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Speculart;
float _Gloss;
struct a2v{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f{
float4 pos:SV_POSITION;
fixed3 color:COLOR;
};
v2f vert(a2v v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLightDir));
fixed3 reflectDir = normalize(reflect(-worldLightDir,worldNormal));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld,v.vertex).xyz);
fixed3 specular = _LightColor0.rgb * _Speculart.rgb * pow(saturate(dot(reflectDir,viewDir)),_Gloss);
o.color = ambient + diffuse + specular;
return o;
}
fixed4 frag(v2f i):SV_Target{
return fixed4(i.color,1.0);
}
ENDCG
}
}
Fallback "Specular"
}
结果图像:
可以看出,逐顶点光照,在顶点处很亮而在三角形中间较暗,看上去很不自然。
逐像素光照Code:
Shader "Unlit/SpecularPixelShader"
{
Properties
{
_Diffuse ("Diffuse", Color) = (1,1,1,1)
_Speculart ("Specular",Color) = (1,1,1,1)
_Gloss ("Gloss", Range(8.0,256)) = 20
}
SubShader
{
Pass
{
Tags { "LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
// #pragma multi_compile_fog
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Speculart;
float _Gloss;
struct a2v{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f{
float4 pos:SV_POSITION;
fixed3 worldNormal:NORMAL;
fixed3 worldPos:TEXCOORD1;
};
v2f vert(a2v v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// o.worldNormal = mul(v.normal,(float3x3)unity_WorldToObject);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 frag(v2f i):SV_Target{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
// fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLightDir));
//phong模型
fixed3 reflectDir = normalize(reflect(-worldLightDir,worldNormal));
// fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos.xyz));
//blinn phong 模型
// fixed3 halfDir = normalize(viewDir + worldLightDir);
fixed3 specular = _LightColor0.rgb * _Speculart.rgb * pow(saturate(dot(reflectDir,viewDir)),_Gloss);
// fixed3 specular = _LightColor0.rgb * _Speculart.rgb * pow(saturate(dot(halfDir,worldNormal)),_Gloss);
fixed3 color = ambient + diffuse + specular;
return fixed4(color,1.0);
}
ENDCG
}
}
Fallback "Specular"
}
实验结果:
上面Phong模型的结果,下面是Blinn Phong模型的结果,可以看到逐像素光照的结果更自然更平滑。