天天看點

OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong

從本節開始,我們可以開始學習OpenGL中包括光照、模型加載等主題。光照是一個複雜的主題,本節學習簡單的Phong reflection model.本節示例程式https://github.com/wangdingqiao/noteForOpenGL/tree/master/lighting。

本節内容整理自: 

1. learnopengl.com Basic Lighting 

2.Modern OpenGL 06 – Diffuse Point Lighting

通過本節可以了解到

  • 顔色與光照的關系
  • 簡單實作的Phong Reflection Model
  • Gouraud shading和Phong Shading的對比

顔色與光照的關系

我們看到的物體的顔色,實際上是光照射物體後反射的光進入眼睛後感受到的顔色,而不是物體實際材料的顔色。太陽的白光包含了所有我們可以感覺的顔色,可以将這個白光通過棱鏡折射後分離為各種顔色的光。一束白光照射到紅顔色的車身上,光經過車身,一部分被吸收,一部分被反射進入人的眼睛,我們感覺到的顔色就是這個反射後進入眼睛的光的顔色,如下圖左所示:

OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong
OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong

圖中,紅色的表面吸收了藍色和綠色成分,将紅色反射出來。顔色吸收和反射的過程可以表示為: 

LightIntensity∗ObjectColor=Reflectcolor  

計算為: 

(R,G,B)∗(X,Y,Z)=(XR,YG,ZB)  

則上面的過程表示為: 

(1,1,1)∗(1,0,0)=(1,0,0)  

如果将cyan (blue + green) 顔色光束照射到車身,車身會是什麼顔色呢?會是黑色的,因為 (0,1,1)∗(1,0,0)=(0,0,0) ,這個過程如上圖右所示

如果将cyan (blue + green) 顔色照射到magenta (red + blue) 顔色的表面,那麼結果會是什麼顔色呢?同理,我們可以得到結果顔色為藍色

在實際場景中,光的強度的各個分量可以在[0,1]之間變化,材料表面的顔色分量也可以在[0,1]之間變化,例如光照射到一個玩具表面的計算過程為:

glm::vec3 lightColor(, , );
glm::vec3 toyColor(, , );
glm::vec3 result = lightColor * toyColor; // = (0.33f, 0.21f, 0.06f);           
  • 1
  • 2
  • 3

Phong Reflection Model

要模拟現實的光照是困難的,例如實際光照中,一束光可以經過場景中若幹物體反射後,照射到目标物體上,也可以是直接照射到目标物體上。其中經過其他物體反射後再次照射到目标物體上,這是一個遞歸的過程,将會無比複雜。是以實際模拟光照過程中,總是采用近似模型去接近現實光照。Phong Reflection Model是經典的光照模型,它計算光照包括三個部分:環境光+漫反射光+鏡面光,一共三個成分,如下圖所示(來自wiki ,作者Brad Smith): 

OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong

環境光成分

環境光是場景中光源給定或者全局給定的一個光照常量,它一般很小,主要是為了模拟即使場景中沒有光照時,也不是全部黑屏的效果。場景中總有一點環境光,不至于使場景全部黑暗,例如遠處的月亮,遠處的光源。 

環境光的實作為:

// 環境光成分
float   ambientStrength = f;
vec3    ambient = ambientStrength * lightColor * objectColor;           
  • 1
  • 2
  • 3

給定環境光後,場景效果如下圖所示: 

OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong

這裡使用了兩個着色器繪圖。一個着色器用來繪制光源,光源用一個縮小的立方體來模拟,如圖中白色立方體所示;另一個着色器用來繪制我們的物體,這裡隻顯示了一個大的立方體。當場景中隻有環境光時,立方體隻能很暗的顯示。

漫反射光成分

漫反射光成分,是光照中的一個主要成分。漫反射光強度與光線入射方向和物體表面的法向量之間的夾角 θ 相關。當 θ  = 0時,物體表面正好垂直于光線方向,這是獲得的光照強度最大;當 θ  = 90時物體表面與光線方向平行,此時光線照射不到物體,光的強度最弱;當 θ>90 後,物體的表面轉向到光線的背面,此時物體對應的表面接受不到光照。入射角度如下圖所示: 

OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong

這裡需要的向量包括: 

1.光源和頂點位置之間的向量L 需要計算。 

2.法向量N 通過頂點屬性裡指定 經過模型和視變換後需要重新計算。

作為本節的簡單示例程式,我們在頂點屬性中指定法向量,例如立方體的正面ABCD這個面的頂點屬性如下所示:

// 頂點屬性 位置 紋理坐标  法向量
GLfloat vertices[] = {
        -, -, , , , , ,,    // A
        , -, , , ,  , , ,   // B
        , , ,  , ,   , , ,  // C
        , , ,  , ,   , , ,  // C
        -, , ,  , ,  , , ,  // D
        -, -, , , , , , ,   // A
    ...省略           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

和頂點位置屬性一樣,我們需要将法向量資料發送到GPU,并且使用glVertexAttribPointer告訴OpenGL資料的解析方式。這些内容在前面已經介紹了,這裡不再贅述。

将頂點屬性傳遞到着色器後,需要在着色器中開始我們的光照計算。有兩種方法執行向量L和N的計算。一種方式是在世界坐标系中計算,另一種是在相機坐标系中計算,兩種方法都可以實作。

在世界坐标系中計算光照

這裡以在世界坐标系中計算L和N為例進行說明,在相機坐标系中計算也有類似操作。在世界坐标系中,計算L時,光源lightPos是在世界坐标系中指定的位置,直接使用即可。頂點位置需要變換到世界坐标系中,利用Model矩陣即可,使用式子: 

FragPos=vec3(model∗vec4(position,1.0));(變換後頂點位置)

在計算N時需要注意,我們不能直接利用 Model∗normal 來擷取變換後的法向量,應該使用式子: 

Normal=mat3(transpose(inverse(model)))∗normal(變換後法向量) 。

這個式子的具體推導過程,可以參考The Normal Matrix。 

綜上所述,頂點着色器中計算頂點位置和法向量代碼為:

#version 330
layout(location = ) in vec3 position;
layout(location = ) in vec2 textCoord;
layout(location = ) in vec3 normal;

out vec3 FragPos;
out vec2 TextCoord;
out vec3 FragNormal;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

void main()
{
gl_Position = projection * view * model * vec4(position, );
FragPos = vec3(model * vec4(position, )); // 在世界坐标系中指定
TextCoord = textCoord;
mat3 normalMatrix = mat3(transpose(inverse(model)));
FragNormal = normalMatrix * normal; // 計算法向量經過模型變換後值
}           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在片元着色器中,計算漫反射光成分的代碼為:

// 漫反射光成分 此時需要光線方向為指向光源
vec3    lightDir = normalize(lightPos - FragPos);
vec3    normal = normalize(FragNormal);
float   diffFactor = max(dot(lightDir, normal), );
vec3    diffuse = diffFactor * lightColor * objectColor;           
  • 1
  • 2
  • 3
  • 4
  • 5

這裡使用max(dot(lightDir, normal), 0.0)主要是為了防止當光線和法向量夾角大于90後,取值為負的情況,是以使用max保證漫反射光照系數在[0.0,1.0]範圍内。 

添加了漫反射光成分後的效果如下圖所示: 

OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong

鏡面反射光成分

鏡面光成分模拟的是物體表面光滑時反射的高亮的光,鏡面光反映的通常是光的顔色,而不是物體的顔色。計算鏡面光成分時,要考慮光源和頂點位置之間向量L、法向量N、反射方向R、觀察者和頂點位置之間的向量V之間的關系,如下圖所示(來自:Lighting and Material): 

OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong

當R和V的夾角 θ 越小時,人眼觀察到的鏡面光成分越明顯。鏡面反射系數定義為: 

specFactor=cos(θ)s  

其中 s 表示為鏡面高光系數(shininess ),它的值一般取為2的整數幂,值越大則高光部分越集中,例如下面圖中,測試了幾種不同的高光系數,效果如下所示: 

OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong

計算鏡面光成分過程為:

// 鏡面反射成分 此時需要光線方向為由光源指出
float   specularStrength = f;
vec3    reflectDir = normalize(reflect(-lightDir, normal));
vec3    viewDir = normalize(viewPos - FragPos);
float   specFactor = pow(max(dot(reflectDir, viewDir), ), ); // 32為鏡面高光系數
vec3    specular = specularStrength * specFactor * lightColor * objectColor;           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

這裡需要注意的是,利用reflect函數計算光的出射方向時,要求入射方向指向物體表面位置,是以這裡翻轉了lightDir,計算為:

vec3 reflectDir = normalize(reflect(-lightDir, normal));           
  • 1

将上述三種光成分疊加後,成為最終物體的顔色,片元着色器中實作為:

vec3 result = ambient + diffuse + specular 
   color = vec4(result , f);           
  • 1
  • 2

繪制效果如下圖所示: 

OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong

per-vertex 和per-fragment實作光照的對比

上面我們實作的光照計算是在片元着色器中進行的,這種是基于片元計算的,稱之為Phong shading。在過去OpenGL程式設計中實作的是在頂點着色器中進行光照計算,這是基元頂點的計算的,稱之為Gouraud Shading。Gouraud Shading和Phong shading,兩者的效果對比如下圖所示(來自learnopengl.com Basic Lighting): 

OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong

基于頂點計算光照的優勢在于頂點數目比片元數目少,是以計算速度快,但是基于頂點計算的光照沒有基元片元的真實,主要是頂點計算時,隻計算了頂點的光照,而其餘片元的光照由插值計算得到,這種插值後的光照顯得不是很真實,需要使用更多的頂點來加以完善。例如下面的圖中,分别顯示了使用少量和大量頂點的基于頂點的光照計算效果:

OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong
OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong

使用基于片元的光照計算時能夠擷取更為平滑的光照效果。實作基元頂點的光照計算過程,即将上述在片元着色器中的光照計算過程遷移到頂點着色器中執行。

Phong不能處理的情況

我們知道,Phong模型在計算鏡面光系數為:

float   specFactor = pow(max(dot(reflectDir, viewDir), ), ); // 32為鏡面高光系數           
  • 1

這裡的計算由反射向量和觀察向量決定,當兩者的夾角 θ 超過90時,截斷為0.0,則沒有了鏡面光成分。是以Phong模型能處理的是下面的左圖中( θ≤90 )的情況,而對于右圖中( θ>90 )的情況則鏡面光成分計算為0(來自Advanced-Lighting)。 

OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong

而右圖的這種情況實際上是存在的,将鏡面光成分取為0,沒有很好地展現實際光照情況。例如下面的圖表示的是,鏡面光系數為1.0,法向量為(0.0,1.0,0.0)的平面位置在-0.5,光源在原點時,觀察者在(0,0,4.0)位置時,光照展示的情形:

OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong

這裡我們看到,Phong的鏡面光成分,在邊緣時立馬變暗,這種對比太明顯,不符合實際情形。

Blinn-Phong

Blinn-Phong模型鏡面光的計算,采用了半角向量(half-angle vector),這個向量是光照向量L和觀察向量V的取中向量,如下圖所示(來自Blinn-Phong Model):

OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong

計算為:  H=L+V||L+V||

當觀察向量與反射向量越接近,那麼半角向量與法向量N越接近,觀察者看到的鏡面光成分越強。

對比Phong和Blinn-Phong計算鏡面光系數為:

vec3   viewDir = normalize(viewPos - fs_in.FragPos);
float   specFactor = ;
if(blinn)  // 使用Blinn-Phong specular 模型
{
vec3 halfDir = normalize(lightDir + viewDir);
specFactor = pow(max(dot(halfDir, normal), ), ); 
}
else    // 使用Phong specular模型
{
vec3    reflectDir = normalize(reflect(-lightDir, normal)); // 此時需要光線方向為由光源指出
specFactor = pow(max(dot(reflectDir, viewDir), ), ); 
}           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

使用半角向量後,保證了半角向量H與法向量N的夾角在90度範圍内,能夠處理上面對比圖中右圖所示的情形。下面左是鏡面高光系數為0.5時使用Blinn-Phong渲染效果:

OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong
OpenGL學習: 光照系列1-光照基礎(phong模型) Phong不能處理的情況 Blinn-Phong

右圖是鏡面高光系數為0.5時使用Phong渲染效果:

一般地,使用Blinn-Phong模型時要得到相同強度的鏡面光,鏡面系數需要為Phong模型的2-4倍,例如Phong模型的鏡面高光系數設定為0.8,可以設定Blinn-Phong模型的系數為32.0。

關于Phong和Blinn-Phong模型更多地對比,可以參考Relationship between Phong and Blinn lighting model。

最後的說明

在計算光照的過程中,注意使用的向量一定要機關化,因為 cosθ 值的計算依賴于兩個參與點積的向量是機關向量這一事實,否則計算會出錯。另外在世界坐标系還是在相機坐标系中進行光照計算都是可以的,這個取決于你的喜好,但是要注意将頂點位置、法向量都變換到同一個坐标系下進行光照計算。

本節實作的Phong reflection model還不夠完善,一方面從光源角度看,屬于點光源,但是缺少随着距離的衰減;另一方面從物體的材質角度看,沒有反映出物體不同部分對光感受的強度不同這一特點,需要使用材質屬性加以改進。這些内容将放在下一節中進行學習。

繼續閱讀