在1975年,著名越南籍学者裴祥风(Bui Tuong Phone)提出了标准光照模型。
它把进入到摄像机内的光线分为4个部分:环境光(ambient)、漫反射(diffuse)、高光反射(或称镜面反射,specular)和自发光(emissive)。
每个部分使用一种方法来计算它的贡献度,然后线性叠加得到最终的光照颜色。
c = cambient + cdiffuse + cspecular + cemissive
Phong着色法(Phong Shading)
基于标准光照模型理论,裴祥风(Bui Tuong Phone)给出了Phong着色法(Phong Shading)的光照计算的完整实现。
Phong着色法基于这样的观察:
(1)一个物体表面越粗糙,其对光线的反射就越分散,而这部分反射的光构成了物体本身的基础颜色,这部分颜色用漫反射分量表示
(2)一个物体表面越光滑,其对光线的反射就越集中,就越会在某些位置上呈现比较集中明亮的高光,这部分颜色用高光反射分量表示
(3)如果场景中有光源,那么即便一个物体没有直接被光源照亮,我们也还是看到这个物体。事实上,这部分表面接收到了来自四面八方的间接光照,这部分颜色用环境光分量表示
Phong着色法虽然是一个经验模型,但它也符合一些基本的物理规律,可以很好地模拟相当广泛的视觉场景。
环境光(ambient)
环境光用来近似模拟间接光照(indrect light),即在多个物体之间反射后,进入摄像机的光线。环境光的计算非常简单,它通常是一个全局变量,即场景中所有物体都使用这个环境光。
cambient = gambient
自发光(emissive)
自发光描述物体表面不经过任何物体的反射,直接进入摄像机的光线。它的计算也很简单,直接使用该材质的自发光颜色,这些自发光的表面并不会真的照亮周围的物体,仅仅是它本身看起来更亮而已。
cemissive = memissive
对于漫反射(diffuse)和高光反射(或称镜面反射,specular)的部分,则只关心直接光照(direct light),即那些直接从光源发射出来照射到物体表面后,经过物体表面的一次反射直接进入摄像机的光线。示意图如下:
L为入射光线,N为表面法线,fd为漫反射光,fs为高光反射光(或镜面反射光)
漫反射(diffuse)
漫反射是被物体表面随机散射到各个方向的光线,可以认为在任何反射方向上的分布都是一样的。因此,无论观察者从哪个方向观察,漫反射效果是一样的,所以我们认为漫反射和观察位置是无关的。
但是,入射光线的角度很重要。漫反射的大小取决于表面法线N和光线L的夹角。光线越水平,夹角越大,漫反射分量越小;当夹角接近90度时,漫反射几乎为0。
漫反射光照符合兰伯特定律(Lambert's law):反射光线的强度与表面法线和光源方向之间的夹角的余弦值成正比,即表面法线和光源方向的点乘。
cdiffuse = clight * mdiffuse * max{0, dot(L, N)}
注:N是归一化的表面法线,L是归一化的光源入射方向,mdiffuse是材质的漫反射颜色,clight是光源颜色。
需要注意的是,我们需要防止dot(N, L)的结果为负值,为此,使用取最大值max函数将其截取到0,这可以防止物体被从后面来的光源照亮。
这里有一个问题:在光照无法到达的区域,模型的外观通常是全黑的,没有任何明暗变化,这会使模型的背光区域看起来就像一个平面一样,失去了模型细节表现。
虽然我们可以通过添加环境光(ambient)来避免全黑,但仍然无法解决背光面没有明暗变化的问题。为此,有一种改善技术被提出来,即:半兰伯特(Half Lambert)光照模型。
半兰伯特(Half Lambert)光照模型是Valve公司在开发游戏《半条命》时提出的一种技术,该技术是在原兰伯特光照模型基础上进行了一个简单的修改。
广义的半兰伯特光照模型的公式如下:
cdiffuse = clight * mdiffuse * (α * dot(L, N) + β)
注:半兰伯特没有使用max函数来防止dot(N, L)的结果为负值。而是对其结果进行了一个α倍的缩放再加上一个β大小的偏移。
绝大多数情况下,α、β会取值0.5,即公式为:
cdiffuse = clight * mdiffuse * (0.5 * dot(L, N) + 0.5)
通过这样的方式,我们把dot(N, L)的结果范围从[-1,1]映射到[0,1]范围内。也就说,对于模型的背光面,在原兰伯特都映射到同一个值0,而在半兰伯特中,背光面映射到了不同的值上,有了明暗的变化。
需要注意的是,半兰伯特是没有物理依据的,仅仅是一个视觉加强技术。
高光反射(或称镜面反射,specular)
高光是那些沿着完全镜面反射方向被反射的光线,这可以让物体看起来是由光泽的。高光反射与观察方向是有关系的,在描述其性质时,需要知道观察者位置信息。
现实生活中我们也会注意到,当看到一个物体表面反射了刺眼的光线的时候,只要我们稍稍错开一点位置,就不会再感到刺眼了。
计算高光反射需要知道的信息比较多,如:表面法线、视角方向、光源方向等。反射方向可以通过表面法线和视角方向计算得到,如下。
L + R = 2 * dot(N, L) * N,则:R = 2 * dot(L, N) * N - L 注:L、R、N都是归一化后的单位向量
使用Phong模型计算高光反射的部分
cspecular = clight * mspecular * max{0, dot(V, R)}mgloss
注1:clight是光源的颜色和强度。mspecular则是材质的高光反射颜色,用于控制该材质对于高光反射的强度和颜色。
mgloss是材质的光泽度(gloss),也被称为反光度(shininess),它用于控制高光区域的“亮点”有多宽, mgloss越大,亮点就越小。
注2:dot(V, R)也需要防止为负数。
计算结果如下图红框所示:
这个公式意味着,反射方向R和观察方向V的夹角一旦超过90度,高光就变成0。
当mgloss较大的时候不会产生太大的影响,因为此时高光衰减很快,还不到90度就已经衰减完了,但是如果mgloss很小,那么高光范围就会很大,我们很容易观察到这个断层。
Phong Shading在unity中的实现如下(Chapter6-SpecularPixelLevel.shader):
Shader "Unity Shaders Book/Chapter 6/Specular Pixel-Level" { Properties { _Diffuse ("Diffuse", Color) = (1, 1, 1, 1) _Specular ("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 #include "Lighting.cginc" fixed4 _Diffuse; // 材质的漫反射颜色 fixed4 _Specular; // 材质的高光反射颜色 float _Gloss; // 材质的光泽度 struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; }; v2f vert(a2v v) { v2f o; // Transform the vertex from object space to projection space o.pos = mul(UNITY_MATRIX_MVP, v.vertex); // Transform the normal from object space to world space o.worldNormal = mul(v.normal, (float3x3)_World2Object); // Transform the vertex from object spacet to world space o.worldPos = mul(_Object2World, v.vertex).xyz; return o; } fixed4 frag(v2f i) : SV_Target { // Get ambient term fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; // 环境光 fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); // Compute diffuse term fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir)); // 兰伯特漫反射光 // Get the reflect direction in world space fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal)); // Get the view direction in world space fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); // Compute specular term fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss); // Phong高光反射光 return fixed4(ambient + diffuse + specular, 1.0); } ENDCG } } FallBack "Specular" }
Blinn-Phong着色法(Blinn-Phong Shading)
需要强调的是,Blinn-Phong着色法在环境光(ambient)、漫反射(diffuse)和自发光(emissive)部分的计算方式和Phong着色法完全一致。
Blinn针对上面Phong高光反射问题提出了Blinn-Phong反射模型,并很好地解决了高光断层问题。
Blinn-Phong模型在大部分情况下看起来会更自然一点,特别是低高光的区域。也是早期固定渲染管线时代时OpenGL所采用的光照模型。
Phong和Blinn都是经验模型,我们不应该认为Blinn模型是对“正确的”Phong模型的近似。
使用Blinn-Phong模型计算高光反射的部分
Blinn-Phong的基本思想是,避免计算反射方向R。为此,Blinn模型引入了一个新的向量H(半程向量,Halfway Vector),它通过对V和L取平均后再归一化得到:
H = (V + L) / ‖ V + L ‖ 注:‖ V + L ‖为对V + L向量和取模,即求V + L向量和的长度
然后,使用N和H之间的夹角进行计算,而非V和R之间的夹角。 注:只要在平面的同一侧,N和H之间的夹角就不会超过90度,从而规避掉了Phong反射模型的问题
cspecular = clight * mspecular * max{0, dot(N, H)}mgloss
Blinn-Phong与Phong的结果对比:
Blinn-Phong Shading在unity中的实现如下(Chapter6-BlinnPhong.shader):
Shader "Unity Shaders Book/Chapter 6/Blinn-Phong" { Properties { _Diffuse ("Diffuse", Color) = (1, 1, 1, 1) _Specular ("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 #include "Lighting.cginc" fixed4 _Diffuse; // 材质的漫反射颜色 fixed4 _Specular; // 材质的高光反射颜色 float _Gloss; // 材质的光泽度 struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; }; v2f vert(a2v v) { v2f o; // Transform the vertex from object space to projection space o.pos = mul(UNITY_MATRIX_MVP, v.vertex); // Transform the normal from object space to world space o.worldNormal = mul(v.normal, (float3x3)_World2Object); // Transform the vertex from object spacet to world space o.worldPos = mul(_Object2World, v.vertex).xyz; return o; } fixed4 frag(v2f i) : SV_Target { // Get ambient term fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; // 环境光 fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); // Compute diffuse term fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir)); // 兰伯特漫反射光 // Get the view direction in world space fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); // Get the half direction in world space fixed3 halfDir = normalize(worldLightDir + viewDir); // Compute specular term fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss); // Blinn-Phong高光反射光 return fixed4(ambient + diffuse + specular, 1.0); } ENDCG } } FallBack "Specular" // 上述SubShader都失败后,回调内置名为Specular的Unity Shader }
unity实现效果对比:
参考
LearnOpenGL(Advanced Lighting)
List of common shading algorithms(wikipedia)
Unity_Shaders_Book(candycat1992)