漫射光

教程18

漫射光

http://ogldev.atspace.co.uk/

原文: http://ogldev.atspace.co.uk/www/tutorial16/tutorial18.html

CSDN完整版專欄: http://blog.csdn.net/column/details/13062.html


背景

漫射光和環境光的主要不同是漫射光的特性依賴光線的方向,而環境光完全忽略光的方向。當只有環境光時整個場景是被均勻照亮的,而漫射光使物體朝向它的那一面比其他背向光的面要更亮。漫射光還增加了一個光強度的變化現象,光的強度大小還取決於光線和物體表面的角度,這個概念可以在下圖看出:

http://ogldev.atspace.co.uk/www/tutorial18/light_angle.png

假想兩邊的光線的長度是一樣的,唯一不同的是他們的方向。按照漫射光的模型,左邊物體的表面要比右邊的物體要亮,因爲右邊光線的入射角比左邊的大很多。實際上左邊的亮度是能達到的最大亮度了,因爲它是垂直入射。

漫射光模型是建立在蘭伯特餘弦定律上的,光想的強度和觀察者視線與物體表面法線夾角的餘弦值成正比(夾角越大光強度越小)。注意這裏略有變化,我麼使用的是光線的方向而不是觀察者視線(在鏡面發射中用到)的方向。

爲了計算光的強度我們要引入光線和物體表面法線(蘭伯特定律中更加通用的概念叫做’directionaly proportional’)夾角的餘弦值作爲一個參數變量。看下面這幅圖:

http://ogldev.atspace.co.uk/www/tutorial18/lambert_law.png

圖中四條光線以不同的角度照到表面上(光線僅僅是方向不同),綠色的箭頭是表面法向量,從表面垂直往外發出。光線A的強度是最大的,因爲光線A和法線的夾角爲0,餘弦值爲最大的1,也就是這個光線的強度(三個通道的三個0-1的值)和表面顏色相乘每個顏色通道都是乘以1,這是漫射光強度最大的情況了。光線B以一定角度(0-90之間)照射到表面,這個角度就是光線和法線的夾角,那麼夾角的餘弦值應該在0-1之間,表面顏色值最後要和這個角度的餘弦值相乘,那麼得到的光的強度一定是比光線A要弱的。

對於光線C和D情況又不同了。C從表面的一側入射,光線和表面的夾角爲0,和法線垂直,對應的餘弦值爲0,這會導致光線C對錶面照亮沒有任何效果。光線D從表面的背面入射和法線成鈍角,餘弦值爲負比0還小甚至小到-1。所以光線D和C一樣都對物體表面沒有照亮作用。

通過上面的分析我們可以得到一個很重要的結論:光線如果要對物體表面的亮度產生影響,那麼光線和法線的角度要在0-90度之間但不包含90度。

可以看到表面法線對漫射光的計算很重要。上面的例子是很簡化的:表面是平坦的直線只需要考慮一條法線。而真實世界中的物體有無數的多邊形組成,每個多邊形的法線和附近的多邊形基本都不一樣。例如:

http://ogldev.atspace.co.uk/www/tutorial18/normals.png

因爲一個多邊形面上分佈的任意法向量都是一樣的,足以用其中一個代表來計算頂點着色器中的漫射光。一個三角形上的三個頂點會有相同的顏色而且整個三角形面的顏色都相同,但這樣看上去效果並不好,每個多邊形之間的顏色值都不一樣這樣我們會看到多邊形之間邊界的顏色變化不平滑,因此這個明顯是需要進行優化的。

優化的辦法中使用到一個概念叫做‘頂點法線’。頂點法線是共用一個頂點的所有三角形法線的平均值。事實上我們並沒有在頂點着色器中計算漫射光顏色,而只是將頂點法線作爲一個成員屬性傳給片段着色器。光柵器會得到三個不同的法向量並對其之間進行插值運算。片段着色器將會對每個像素計算其特定的插值法向量對應的顏色值。這樣使用那個插值後得到的每個像素特定法向量,我們對漫射光的計算可以達到像素級別。效果是光照效果在每個相鄰三角形面之間會平滑的變化。這種技術叫做Phong着色(Phong Shading)。下面是頂點法線插值後的樣子:

http://ogldev.atspace.co.uk/www/tutorial18/vertex_normals.png

但是我們會發現之前教程用的那個金字塔模型使用上面這些插值後的法向量計算優化後看上去很奇怪,有點想還是用本來沒插值的法向量。這裏是因爲金字塔面很少,在後面更復雜的模型中使用上面的插值優化方法模型就會看上去更加平滑真實。

最有一點要關心的是漫射光計算所在的座標空間。頂點和他們的法線都定義在本地座標系空間,並且都在頂點着色器中被我們提供給shader的WVP矩陣進行了變換,然後到裁剪空間。然而,在世界座標系中來定義光線的方向纔是最合理的,畢竟光線的方向決定於世界空間中某個地方的光源將光線投射到某個方向(甚至太陽都是在世界空間中,只是距離極遠)。所以,在計算之前,我們首先要將法線向量變換到世界座標系空間。

代碼詳解

(lighting_technique.h:25)

struct DirectionalLight
{
    Vector3f Color;
    float AmbientIntensity;
    Vector3f Direction;
    float DiffuseIntensity;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

這是新的平行光的數據結構,有兩個新的成員變量:方向是定義在世界空間的一個3維向量,漫射光光照強度是一個浮點數(和環境光的用法一樣)。

(lighting.vs)

#version 330

layout (location = 0) in vec3 Position;
layout (location = 1) in vec2 TexCoord;
layout (location = 2) in vec3 Normal;

uniform mat4 gWVP;
uniform mat4 gWorld;

out vec2 TexCoord0;
out vec3 Normal0;

void main()
{
    gl_Position = gWVP * vec4(Position, 1.0);
    TexCoord0 = TexCoord;
    Normal0 = (gWorld * vec4(Normal, 0.0)).xyz;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

這是更新了的頂點着色器,有一個新的頂點屬性:法向量,這個法向量要由應用程序提供。另外世界變換有其自己的一致變量我們要和WVP變換矩陣一併提供。頂點着色器通過世界世界變換矩陣將法向量變換到世界空間並傳遞給片斷着色器。注意這時候3維向量擴展成了4維向量,和4維世界變換矩陣相乘後又降回3維(…).xyz。GLSL的這種能力稱作調配‘swizzling’,使向量操作非常靈活。比如,一個3維向量v(1,2,3),那麼vec4 n = v.zzyy中4維向量n的內容爲:(3,3,2,2)。注意如果我們要擴展3維向量到4維我們必須將第四個分量設置爲0,這會使世界變換矩陣的變換效果(第4列)失效,因爲向量不能像點一樣移動,之只能縮放和旋轉。

(lighting.fs:1)
#version 330

in vec2 TexCoord0;
in vec3 Normal0;

out vec4 FragColor;

struct DirectionalLight
{
    vec3 Color;
    float AmbientIntensity;
    float DiffuseIntensity;
    vec3 Direction;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

這裏是片段着色器的開始,它現在接受到插值後並在頂點着色器中轉換到世界空間的頂點法向量。平行光的數據結構也擴展了來和C++中的對應匹配並且包含了新的光屬性。

(lighting.fs:19)

void main()
{
    vec4 AmbientColor = vec4(gDirectionalLight.Color * gDirectionalLight.AmbientIntensity, 1.0f);
    // 環境光顏色參數的計算沒有變化,我們計算並存儲它然後用在下面最終的公式中。

    float DiffuseFactor = dot(normalize(Normal0), -gDirectionalLight.Direction);
    // 這是漫射光計算的核心。我們通過對光源向量和法線向量做點積計算他們之間夾角的餘弦值。這裏有三個注意點:
    1. 從頂點着色器傳來的法向量在使用之前是經過單位化了的,因爲經過插值之後法線向量的長度可能會變化不再是單位向量了;
    2.光源的方向被反過來了,因爲本來光線垂直照射到表上時的方向和法線向量實際是相反的成180度角,計算的時候將光源方向取反那麼垂直入射時和法線夾角爲0,這時才和我們的計算相符合。
    3.光源向量不是單位化的。如果對所有像素的同一個向量都進行反覆單位化會很浪費GPU資源。因此我們只要保證應用程序傳遞的向量在draw call之前被單位化即可。

    vec4 DiffuseColor;
    if (DiffuseFactor > 0) {
        DiffuseColor = vec4(gDirectionalLight.Color * gDirectionalLight.DiffuseIntensity * DiffuseFactor, 1.0f);
    }
    else {
        DiffuseColor = vec4(0, 0, 0, 0);
    }

    // 這裏我們根據光的顏色、漫射光強度和光的方向來計算漫射光的部分。如果漫射參數是負的或者爲0意味着光線是以一個鈍角射到物體表面的(從水平一側或者表面的背面),這時候光照是沒有效果的同時漫射光的顏色參數會被初始化設置爲零。如果夾角大於0我們就可以進行計算漫射光的顏色值了,將基本的顏色值和漫射光強度常量相乘,最後使用漫射參數DiffuseFactor對最後結果進行縮放。如果光是垂直入射那麼漫射參數會是1,光的亮度最大。

    FragColor = texture2D(gSampler, TexCoord0.xy) * (AmbientColor + DiffuseColor);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

這是最後的光照計算了。我們加入了環境光和漫射光的部分,並將結果和從紋理中取樣得到的顏色相乘。現在可以看到即使漫射光方向太偏(照到反面或者從水平一側)沒有對錶面起到照亮效果,環境光仍然能照亮物體,當然環境光也得要存在。

(lighting_technique.cpp:144)

void LightingTechnique::SetDirectionalLight(const DirectionalLight& Light)
{
    glUniform3f(m_dirLightLocation.Color, Light.Color.x, Light.Color.y, Light.Color.z);
    glUniform1f(m_dirLightLocation.AmbientIntensity, Light.AmbientIntensity);
    Vector3f Direction = Light.Direction;
    Direction.Normalize();
    glUniform3f(m_dirLightLocation.Direction, Direction.x, Direction.y, Direction.z);
    glUniform1f(m_dirLightLocation.DiffuseIntensity, Light.DiffuseIntensity);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

這個函數將平行光的參數傳遞到着色器中,可以看到平行光的數據結構經過擴展後既包含了光的方向向量還包含了漫射光強度。注意方向向量在設置到shader着色器之前已經經過了單位化處理。同時LightingTechnique類也獲取了光的方向和強度一致變量的的位置,也獲得了世界變換矩陣的位置,另外還有一個設置世界變換矩陣的函數。這些目前都很常規沒有放太多代碼解釋,具體的可以在源碼裏看。

tutorial18.cpp:35)

struct Vertex
{
    Vector3f m_pos;
    Vector2f m_tex;
    Vector3f m_normal;

    Vertex() {}

    Vertex(Vector3f pos, Vector2f tex)
    {
        m_pos = pos;
        m_tex = tex;
        m_normal = Vector3f(0.0f, 0.0f, 0.0f);
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

這裏最新的頂點的數據結構現在包含了法線向量,構造函數自動將其初始化爲零,並且我們有一個專門的函數來遍歷掃描所有的頂點並計算法向量。

(tutorial18.cpp:197)

void CalcNormals(const unsigned int* pIndices, unsigned int IndexCount, Vertex* pVertices, unsigned int VertexCount)
{
    for (unsigned int i = 0 ; i < IndexCount ; i += 3) {
        unsigned int Index0 = pIndices[i];
        unsigned int Index1 = pIndices[i + 1];
        unsigned int Index2 = pIndices[i + 2];
        Vector3f v1 = pVertices[Index1].m_pos - pVertices[Index0].m_pos;
        Vector3f v2 = pVertices[Index2].m_pos - pVertices[Index0].m_pos;
        Vector3f Normal = v1.Cross(v2);
        Normal.Normalize();

        pVertices[Index0].m_normal += Normal;
        pVertices[Index1].m_normal += Normal;
        pVertices[Index2].m_normal += Normal;
    }

    for (unsigned int i = 0 ; i < VertexCount ; i++) {
        pVertices[i].m_normal.Normalize();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

這個函數參數取得了頂點數組和索引數組,根據索引搜索取出每個三角形的三個頂點並計算其法向量。第一個循環中我們只累加計算三角形每個頂點的法向量。對於每個三角形法向量都是通過計算從第一個頂點出發到其他兩個頂點的兩條邊向量的差積得到的。在向量累加之前要求先將其單位化,因爲差積運算後的結果不一定是單位向量。第二個循環中,我們只遍歷頂點數組(索引我們不關心了)並單位化每個頂點的法向量。這樣操作等同於將累加的向量進行平均處理並留下一個爲單位長度的頂點法線。這個函數在頂點緩衝器創建之前調用,在緩衝器中計算頂點法線,當然此時緩衝期中還會計算其他的一些頂點屬性。

(tutorial18.cpp:131)

    const Matrix4f& WorldTransformation = p.GetWorldTrans();
    m_pEffect->SetWorldMatrix(WorldTransformation);
    ...
    glEnableVertexAttribArray(2);
    ...
    glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)20);
    ...
    glDisableVertexAttribArray(2);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

這是渲染循環中的主要變化,管線類有一個新的函數來提供世界變換矩陣(另外還有WVP變換矩陣)。世界變換矩陣是在縮放變換、旋轉變換和平移變換時計算的。我們可以啓用或禁用第三個頂點屬性數組並且可以定義每個頂點法向量在頂點緩衝器中的偏移值。這裏偏移值爲20,因爲之前已經被位置向量佔用了12bytes,紋理座標佔用了8bytes。

爲了實現這個教程中的圖片效果我們還要定義漫射光強度和光的方向,這個是在tutorial18類中的構造函數中完成的。漫射光強度設置爲0.8,光的方向從左向右。環境光強度逐漸動態地衰弱到0來增強體現漫射光的效果。你可以使用鍵盤的z和x鍵來調整漫射光強度(之前教程中是使用a和s鍵來調整環境光強度的)。

數學理論提示

很多網上的資源說要使用世界變換矩陣逆矩陣的轉置來變換法向量,雖然沒錯,但我們通常不需要考慮那麼遠。我們的世界矩陣總是正交的(他們的向量都正交)。由於正交矩陣的逆和正交矩陣的轉置是相同的,那麼正交矩陣逆的轉置實際就是其轉置的轉置,所以還是本來的矩陣。只要我們避免讓圖形變形扭曲(不成比例的在各軸線上進行縮放)那麼就不會有問題的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章