Phong光照模型

一、Phong光照模型理论

Lambert模型可以较好的模拟出粗糙物体表面的光照效果,但是真实环境还存在着许多表面光滑的物体,例如金属、陶瓷等,Lambert光照模型无法较好地表现。

所以在1975年,Bui Tuong Phong提出了一种局部光照的经验模型,他认为物体表面反射光线由三部分组成:

SurfaceColor=CAmbient+CDiffuse+CSpecularSurfaceColor=C_{Ambient}+C_{Diffuse}+C_{Specular}

其中:

·CAmbientC_{Ambient}:环境光

·CDiffuseC_{Diffuse}:漫反射

·CSpecularC_{Specular}:镜面反射

物体除了受灯光影响,还会收到环境光(Ambient)影响。

当光线照射到一个表面光滑的物体时,除了有漫反射光之外,从某个角度还可以看到很强的反射光。

在接近镜面反射角的一个固定范围内,大部分入射光会被反射,这种现象被称为镜面反射。

镜面反射

镜面反射计算公式

CSpecular=(ClightMSpecular)saturate(vr)MshininessC_{Specular}=(C_{light} \cdot M_{Specular})saturate(v \cdot r)^{M_{shininess}}

其中:

·ClightC_{light}:灯光亮度

·MSpecularM_{Specular}:物体材质的镜面反射颜色

·vv:视角方向(由顶点指向摄像机)

·rr:光线的反射方向

·MshininessM_{shininess}:物体材质的光泽度

二、Shader中获取环境光变量

Unity中有一些可以直接使用的环境光变量,在UnityShaderVariables.cginc中被声明。

Shader中可以使用的环境光变量
变量 类型 说明
unity_AmbientSky fixed4 Gradient类型环境中的Sky Color
unity_AmbientEquator fixed4 Gradient类型环境中的Equator Color
unity_AmbientGround fixed4 Gradient类型环境中的Ground Color
UNITY_LIGHTMODEL_AMBIENT fixed4 Gradient类型环境中的Sky Color,将被unity_AmbientSky取代

依次点击Windows->Rendering->Lighting Setting即可查看当前场景环境光。

需要把默认的SkyBox类型改为Gradient类型才能使用上述变量。

环境光设置

三、基于Phong光照模型的Shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
Shader "Custom/Phong"
{
Properties
{
_MainColor("Main Color",Color)=(1,1,1,1)
_SpecularColor("Specular Color",Color)=(0,0,0,0) //高光颜色
_Shininess("Shininess",Range(1,100))=1 //光泽度
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
//声明包含灯光变量的文件
#include "UnityLightingCommon.cginc"

struct v2f
{
float4 pos:SV_POSITION;
fixed4 color:COLOR0;
};

fixed4 _MainColor;
fixed4 _SpecularColor;
half _Shininess;

v2f vert(appdata_base v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);

//计算公式中的变量
float3 n=UnityObjectToWorldNormal(v.normal);
n=normalize(n);
fixed3 l=normalize(_WorldSpaceLightPos0.xyz);
fixed3 view=normalize(WorldSpaceViewDir(v.vertex));

//漫反射
fixed ndot1=dot(n,l);
fixed4 dif=_LightColor0*_MainColor*saturate(ndot1);

//镜面反射
float3 ref=reflect(-l,n); //计算反射向量
ref=normalize(ref);
fixed rdotv=saturate(dot(view,ref));
fixed4 spec=_LightColor0*_SpecularColor*pow(rdotv,_Shininess);

//环境光+漫反射+镜面反射
o.color=unity_AmbientSky+dif+spec;

return o;
}

fixed4 frag(v2f i):SV_Target
{
return i.color;
}

ENDCG
}
}
}

Phong渲染效果

四、逐像素光照

将Phong光照模型应用于Shader法线高光点边缘并不圆滑,是因为光照模型计算使用的是逐顶点(Per-Vertex)光照,而不是逐像素(Per-Pixel)光照。

对于细分较高的模型,逐顶点光照基本满足需求,但对于多边形数量较少的模型却不行,高光效果不理想。

逐像素光照使用像素着色器,颜色计算基于像素的计算,所以最终屏幕分辨率越高计算量越大。

将上面对Phong代码进行修改:

由于不是在顶点着色器中计算光照颜色,所以v2f不需要保存顶点色,而是需要保存世界空间法线向量和顶点坐标。

1
2
3
4
5
6
struct v2f
{
float4 pos:SV_POSITION;
float3 normal:TEXCOORD0;
float4 vertex:TEXCOORD1;
};

在顶点着色器中,将世界空间法线向量和顶点坐标传递给片段着色器。

1
2
3
4
5
6
7
8
9
v2f vert(appdata_base v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.normal=v.normal;
o.vertex=v.vertex;

return o;
}

在片段着色器中进行计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fixed4 frag(v2f i):SV_Target
{
//计算公式中的所有变量
float3 n=UnityObjectToWorldNormal(i.normal);
n=normalize(n);
fixed3 l=normalize(_WorldSpaceLightPos0.xyz);
fixed3 view=normalize(WorldSpaceViewDir(i.vertex));

//漫反射
fixed ndotl=saturate(dot(n,l));
fixed4 dif=_LightColor0*_MainColor*ndotl;

//镜面反射
float3 ref=reflect(-l,n);
ref=normalize(ref);
fixed rdotv=saturate(dot(ref,view));
fixed4 spec=_LightColor0*_SpecularColor*pow(rdotv,_Shininess);

return unity_AmbientSky+dif+spec;
}

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
Shader "Custom/Per-Pixel Phong"
{
Properties
{
_MainColor("Main Color",Color)=(1,1,1,1)
_SpecularColor("Specular Color",Color)=(0,0,0,0) //高光颜色
_Shininess("Shininess",Range(1,100))=1 //光泽度
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
//声明包含灯光变量的文件
#include "UnityLightingCommon.cginc"

struct v2f
{
float4 pos:SV_POSITION;
float3 normal:TEXCOORD0;
float4 vertex:TEXCOORD1;
};

fixed4 _MainColor;
fixed4 _SpecularColor;
half _Shininess;

v2f vert(appdata_base v)
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.normal=v.normal;
o.vertex=v.vertex;

return o;
}

fixed4 frag(v2f i):SV_Target
{
//计算公式中的所有变量
float3 n=UnityObjectToWorldNormal(i.normal);
n=normalize(n);
fixed3 l=normalize(_WorldSpaceLightPos0.xyz);
fixed3 view=normalize(WorldSpaceViewDir(i.vertex));

//漫反射
fixed ndotl=saturate(dot(n,l));
fixed4 dif=_LightColor0*_MainColor*ndotl;

//镜面反射
float3 ref=reflect(-l,n);
ref=normalize(ref);
fixed rdotv=saturate(dot(ref,view));
fixed4 spec=_LightColor0*_SpecularColor*pow(rdotv,_Shininess);

return unity_AmbientSky+dif+spec;
}

ENDCG
}
}
}

![Per-Pixel Phong渲染效果](Per-Pixel Phong渲染效果.png)

二者对比:

二者对比

可以发现,逐像素光照效果比逐顶点光照更加细腻。

五、Blinn-Phong光照模型

1977年,Jim Blinn对Phong光照模型进行了算法优化,提出了Blinn Phong光照模型。

Blinn-Phong光照模型不再使用反射向量r计算镜面反射,而是使用半角向量 hh 代替 rrhh 为表示角方向 vv 和灯光 ll 的角平分线方向。

![Blinn Phong 镜面反射](Blinn Phong 镜面反射.JPG)

半角向量的计算公式

h=normalize(v+l)h=normalize(v+l)

Blinn-Phong镜面反射计算公式

CSpecular=(ClightMSpecular)saturate(nh)MshininessC_{Specular}=(C_{light} \cdot M_{Specular})saturate(n \cdot h)^{M_{shininess}}

将片段着色器代码进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fixed4 frag(v2f i):SV_Target
{
//计算公式中的所有变量
float3 n=UnityObjectToWorldNormal(i.normal);
n=normalize(n);
fixed3 l=normalize(_WorldSpaceLightPos0.xyz);
fixed3 view=normalize(WorldSpaceViewDir(i.vertex));

//漫反射
fixed ndotl=saturate(dot(n,l));
fixed4 dif=_LightColor0*_MainColor*ndotl;

//镜面反射
fixed3 h=normalize(l+view);
fixed ndoth=saturate(dot(n,h));
fixed4 spec=_LightColor0*_SpecularColor*pow(ndoth,_Shininess);

return unity_AmbientSky+dif+spec;
}

效果图:

Blinn-Phong渲染效果

二者视觉效果差距不大,但是计算性能被优化,减少计算了。