Android OpenGL ES 2.0(二)---環境光和漫射光

本文從下面鏈接翻譯過來:

Android Lesson Two: Ambient and Diffuse Lighting

歡迎學習第二個教程。在本課中,我們將學習如何使用着色器實現Lambertian反射,也稱爲標準漫反射光。 在OpenGL ES2中,我們需要實現自己的光照算法,因此我們將要學習數學以及如何將它應用於場景。

假設和先決條件

本系列的每節課都以前面的課程爲基礎。在開始之前,請查看第一課,因爲本課程將基於其中介紹的概念。

什麼是光?

事實上,沒有光照的世界將是一個昏暗的世界。 沒有光照,我們甚至無法感知世界或我們周圍的物體,除了通過聲音和觸覺等其他感官。 光照向我們展示了物體的明亮或暗淡,它有多近或多遠,以及它所處的角度。

在現實世界中,我們所感知的光線實際上是數萬億微小粒子聚集在一起的光子,光子從光源中飛出,反射數千或數百萬次,並最終到達我們的眼睛,我們將其視爲光。

我們如何通過計算機圖形模擬光照的效果? 有兩種流行的方法:光線跟蹤光柵化。 光線跟蹤的工作原理是通過數學方式跟蹤實際的光線並查看它們的最終位置。 這種技術可以提供非常準確和逼真的結果,但缺點是模擬所有這些光線的計算成本非常高,而且通常對於實時渲染而言太慢。 由於這種限制,大多數實時計算機圖形使用光柵化,而是通過近似結果來模擬光照。 鑑於遊戲的真實性,光柵化也看起來非常好,即使在手機上也可以快速實現實時圖形。 Open GL ES主要是一個光柵化庫,因此這是我們將關注的方法。

不同種類的光

事實證明,我們可以抽象出光的工作方式,並提出三種基本的光:

環境光
這是一個基本的光照,環境光遍佈整個場景。它是不來自任何光源的光線,因爲它在到達你之前已經反彈太多了。這種類型的光照可以在陰天的戶外體驗,或在室內作爲許多不同光源的累積效果。 我們可以爲對象或場景設置相同的環境光,而不是計算獨立的光照,比如漫射光或者鏡面光。

漫射光
這是直接從物體反射後到達您眼睛的光。物體的光照強度隨着光照的角度而變化。 面向光源的物體比某個角度物體更亮。 此外,無論我們相對於物體的角度如何,我們都認爲物體具有相同的亮度。 這也被稱爲Lambert’s cosine law。 漫射光或朗伯反射在日常生活中很常見,並且可以在室內光照亮的白牆上輕鬆看到。
鏡面光
與漫射光不同,當我們相對於物體移動時,鏡面光照會發生變化。 這給物體帶來“光澤”,並且可以在“更光滑”的表面上看到,例如玻璃和其他有光澤的物體。

模擬光源

正如3D場景中有三種主要類型的光一樣,還有三種主要類型的光源:定向光源,點光源和聚光燈。 這些也可以在日常生活中輕鬆看到。

定向光源
定向光源通常來自一個很遠的光源,它可以均勻地照亮整個場景並達到相同的亮度。這種光源是最簡單的類型,因爲無論您在場景中的哪個位置,光線都具有相同的強度和方向。
點光源
點光源可以添加到場景中,以提供更多樣化和逼真的照明。 點光的照射隨距離而下降,並且其光線在所有方向上向外傳播,點光在中心。
聚光燈
除了點光源的特性之外,聚光燈還具有光衰減的方向,通常呈錐形。

數學

在本課中,我們將學習來自點光源的環境光和漫射光。

環境光

環境光實際上是間接漫射光,但它可以被認爲是遍佈整個場景低微的光。如果我們這樣想,那麼計算起來就很容易:
final color = material color * ambient light color
例如,假設我們的物體是紅色的,我們的環境光線是暗淡的白色。假設我們使用RGB顏色模型將顏色存儲爲三種顏色的數組:紅色,綠色和藍色:

final color = {1, 0, 0} * {0.1, 0.1, 0.1} = {0.1, 0.0, 0.0}

物體的最終顏色將是暗紅色,如果您想模擬一個昏暗的白光照亮的紅色物體,這就是您所期望的。環境光除此之外沒什麼,除非你想進入更先進的光照技術,如光能傳遞。

漫射光 - 點光源

對於漫射光,我們需要增加衰減和光照位置。燈光位置將用於計算燈光與物體表面之間的角度,這將影響物體表面的整體光照水平。 它還將用於計算光與物體表面之間的距離,這決定了該點處光的強度。

第1步:計算朗伯因子(lambert factor)。

我們需要做的第一個主要計算是計算出表面和光線之間的角度。面向光直射的表面應該以全強度照射,而傾斜的表面應該得到較少的照射。計算這個的正確方法是使用Lambert的餘弦定律。如果我們有兩個向量,一個是從光到表面上的一個點,第二個是表面法線(如果表面是一個平面,那麼表面法線是一個向上指向或正交於該表面的向量)然後我們可以通過首先對每個向量進行歸一化使其長度爲1,然後通過計算兩個向量的點積來計算餘弦。 這是一個可以通過OpenGL ES2着色器輕鬆完成的操作。

我們稱之爲朗伯因子,它的範圍在0到1之間。

light vector = light position - object position
cosine = dot product(object normal, normalize(light vector))
lambert factor = max(cosine, 0)

這是一個在原點處的平面並且表面法線指向天空的示例。 燈的位置爲{0,10,-10},我們想要計算光向量:

light vector = {0, 10, -10} - {0, 0, 0} = {0, 10, -10}
object normal = {0, 1, 0}

爲了標準化向量,我們將每個分量除以向量長度:

light vector length = square root(0*0 + 10*10 + -10*-10) = square root(200) = 14.14
normalized light vector = {0, 10/14.14, -10/14.14} = {0, 0.707, -0.707}

然後我們計算點積:

dot product({0, 1, 0}, {0, 0.707, -0.707}) = (0 * 0) + (1 * 0.707) + (0 * -0.707) = 0 + 0.707 + 0 = 0.707

這是對點積及其計算結果的一個很好的解釋。

最後,我們限制範圍:

lambert factor = max(0.707, 0) = 0.707

OpenGL ES2的着色器語言內置了對其中一些函數的支持,因此我們不需要手動完成所有數學運算,但它仍然有助於理解正在發生的事情。

第2步:計算衰減係數。

接下來,我們需要計算衰減。 來自點光源的實際光衰減遵循平方反比定律,其也可以表示爲:

luminosity = 1 / (distance * distance)

回到我們的例子,因爲我們的距離爲14.14,這就是我們最終的luminosity

luminosity = 1 / (14.14*14.14) = 1 / 200 = 0.005

如您所見,平方反比定律可以導致距離的強烈衰減。這就是來自點光源的光在現實世界中的工作方式,但由於我們的圖形顯示器具有有限的範圍,因此抑制此衰減係數可能非常有用,我們仍可獲得逼真的照明,而不會看起來太暗。

第3步:計算最終顏色。

既然我們有餘弦和衰減,我們就可以計算出最終的照度:

final color = material color * (light color * lambert factor * luminosity)

繼續我們之前的紅色材料和全白光源示例,這是最終的計算:

final color = {1, 0, 0} * ({1, 1, 1} * 0.707 * 0.005}) = {1, 0, 0} * {0.0035, 0.0035, 0.0035} = {0.0035, 0, 0}

回顧一下,對於漫射光,我們需要使用表面和光線之間的角度以及表面和光線之間的距離,以便計算最終的整體漫射光。 以下是步驟:

//Step one
light vector = light position - object position
cosine = dot product(object normal, normalize(light vector))
lambert factor = max(cosine, 0)
 
//Step two
luminosity = 1 / (distance * distance)
 
//Step three
final color = material color * (light color * lambert factor * luminosity)

將這一切都放入OpenGL ES 2着色器中

頂點着色器

    final String vertexShader =
            "uniform mat4 u_MVPMatrix;      \n"     // A constant representing the combined model/view/projection matrix.
                    + "uniform mat4 u_MVMatrix;       \n"     // A constant representing the combined model/view matrix.
                    + "uniform vec3 u_LightPos;       \n"     // The position of the light in eye space.

                    + "attribute vec4 a_Position;     \n"     // Per-vertex position information we will pass in.
                    + "attribute vec4 a_Color;        \n"     // Per-vertex color information we will pass in.
                    + "attribute vec3 a_Normal;       \n"     // Per-vertex normal information we will pass in.

                    + "varying vec4 v_Color;          \n"     // This will be passed into the fragment shader.

                    + "void main()                    \n"     // The entry point for our vertex shader.
                    + "{                              \n"
// Transform the vertex into eye space.
                    + "   vec3 modelViewVertex = vec3(u_MVMatrix * a_Position);              \n"
// Transform the normal's orientation into eye space.
                    + "   vec3 modelViewNormal = vec3(u_MVMatrix * vec4(a_Normal, 0.0));     \n"
// Will be used for attenuation.
                    + "   float distance = length(u_LightPos - modelViewVertex);             \n"
// Get a lighting direction vector from the light to the vertex.
                    + "   vec3 lightVector = normalize(u_LightPos - modelViewVertex);        \n"
// Calculate the dot product of the light vector and vertex normal. If the normal and light vector are
// pointing in the same direction then it will get max illumination.
                    + "   float diffuse = max(dot(modelViewNormal, lightVector), 0.1);       \n"
// Attenuate the light based on distance.
                    + "   diffuse = diffuse * (1.0 / (1.0 + (0.25 * distance * distance)));  \n"
// Multiply the color by the illumination level. It will be interpolated across the triangle.
                    + "   v_Color = a_Color * diffuse;                                       \n"
// gl_Position is a special variable used to store the final position.
// Multiply the vertex by the matrix to get the final point in normalized screen coordinates.
                    + "   gl_Position = u_MVPMatrix * a_Position;                            \n"
                    + "}                                                                     \n";

這裏有很多事情要做。有我們在第一課中模型/視圖/投影矩陣,但我們還添加了一個模型/視圖矩陣。 爲什麼? 我們需要這個矩陣來計算光源位置和當前頂點位置之間的距離。對於漫射光,只要您可以計算適當的距離和角度,使用世界空間(模型矩陣)或眼睛空間(模型/視圖矩陣)實際上無關緊要,我們課程是在眼睛空間完成計算。

我們傳入頂點顏色和位置信息,以及法線。 我們將最終顏色傳遞給片元着色器,片元着色器將在頂點之間進行插值計算。 這也稱爲Gouraud shading

讓我們看看頂點着色器的每個部分發生了什麼:

// Transform the vertex into eye space.
  + "   vec3 modelViewVertex = vec3(u_MVMatrix * a_Position);              \n"

由於我們在眼睛空間中傳遞光的位置,我們將當前頂點位置轉換爲眼睛空間中的座標,以便我們可以計算適當的距離和角度。

// Transform the normal's orientation into eye space.
  + "   vec3 modelViewNormal = vec3(u_MVMatrix * vec4(a_Normal, 0.0));     \n"

我們還需要改變法線的方向。 這裏我們只是像位置那樣進行常規矩陣乘法,但是如果模型或視圖矩陣已經縮放或傾斜,這將不起作用:我們實際上需要通過將法線乘以原始矩陣的逆轉置矩陣來消除傾斜或縮放的影響。這個網站最好地解釋了爲什麼我們必須這樣做。

關於爲什麼要使用原始矩陣的逆轉置矩陣,可以參考我的另外一篇文章Android OpenGL ES 2.0(九)---法線矩陣

// Will be used for attenuation.
  + "   float distance = length(u_LightPos - modelViewVertex);             \n"

如前面數學部分所示,我們需要距離來計算衰減係數。

// Get a lighting direction vector from the light to the vertex.
  + "   vec3 lightVector = normalize(u_LightPos - modelViewVertex);        \n"

我們還需要光向量來計算朗伯反射係數。

// Calculate the dot product of the light vector and vertex normal. If the normal and light vector are
// pointing in the same direction then it will get max illumination.
  + "   float diffuse = max(dot(modelViewNormal, lightVector), 0.1);       \n"

這與數學部分中的數學運算相同,只是在OpenGL ES2着色器中完成。 最後的0.1只是一種非常便宜的環境照明方式(該值將被限制到最小值0.1)。

// Attenuate the light based on distance.
  + "   diffuse = diffuse * (1.0 / (1.0 + (0.25 * distance * distance)));  \n"

在數學部分,衰減數學與上述略有不同。我們將距離的平方縮放0.25以抑制衰減效果,並且我們還將1.0添加到修改的距離,以便當光非常接近物體時我們不會過渡飽和(否則,當距離小於1時,這個等式實際上會一直照亮而不會衰減它)。

// Multiply the color by the illumination level. It will be interpolated across the triangle.
  + "   v_Color = a_Color * diffuse;                                       \n"
// gl_Position is a special variable used to store the final position.
// Multiply the vertex by the matrix to get the final point in normalized screen coordinates.
  + "   gl_Position = u_MVPMatrix * a_Position;                            \n"

一旦我們得到了最終的光照顏色,我們將它乘以頂點顏色以獲得最終的輸出顏色,然後我們將該頂點的位置投影到屏幕上。

片元着色器

    final String fragmentShader =
            "precision mediump float;       \n"     // Set the default precision to medium. We don't need as high of a
                    // precision in the fragment shader.
                    + "varying vec4 v_Color;          \n"     // This is the color from the vertex shader interpolated across the
                    // triangle per fragment.
                    + "void main()                    \n"     // The entry point for our fragment shader.
                    + "{                              \n"
                    + "   gl_FragColor = v_Color;     \n"     // Pass the color directly through the pipeline.
                    + "}                              \n";

因爲我們在每個頂點的基礎上計算光,我們的片元着色器看起來和第一課中的相同 - 我們所做的就是直接傳遞顏色。 在下一課中,我們將介紹每像素光照。

在本課中,我們專注於實現每頂點光照。 對於具有光滑表面的對象(例如地形)或具有許多三角形的對象的漫反射照明,這通常是足夠好的。 但是,當您的對象不包含許多頂點(例如本示例程序中的立方體)或具有尖角時,頂點光照可能會導致僞影,因爲光線在多邊形上進行線性插值; 當鏡面高光添加到圖像時,這些僞影也變得更加明顯。 更多關於Gouraud shading文章可以到Wiki看到。

解釋程序的變化

除了添加每頂點光照外,該程序還有其他變化。 我們已經從顯示幾個三角形切換到幾個立方體,我們還添加了實用程序函數來加載着色器程序。 還有一些新的着色器可以將光的位置顯示爲一個點,以及其他各種小的變化。

立方體的構造

在第一課中,我們將位置和顏色屬性打包到同一個數組中,但OpenGL ES2還允許我們在單獨的數組中指定這些屬性:

// X, Y, Z
final float[] cubePositionData =
{
        // In OpenGL counter-clockwise winding is default. This means that when we look at a triangle,
        // if the points are counter-clockwise we are looking at the "front". If not we are looking at
        // the back. OpenGL has an optimization where all back-facing triangles are culled, since they
        // usually represent the backside of an object and aren't visible anyways.
 
        // Front face
        -1.0f, 1.0f, 1.0f,
        -1.0f, -1.0f, 1.0f,
        1.0f, 1.0f, 1.0f,
        -1.0f, -1.0f, 1.0f,
        1.0f, -1.0f, 1.0f,
        1.0f, 1.0f, 1.0f,
        ...
 
// R, G, B, A
final float[] cubeColorData =
{
        // Front face (red)
        1.0f, 0.0f, 0.0f, 1.0f,
        1.0f, 0.0f, 0.0f, 1.0f,
        1.0f, 0.0f, 0.0f, 1.0f,
        1.0f, 0.0f, 0.0f, 1.0f,
        1.0f, 0.0f, 0.0f, 1.0f,
        1.0f, 0.0f, 0.0f, 1.0f,
        ...

新的OpenGL標誌

我們還通過glEnable()調用啓用了剔除和深度緩衝:

// Use culling to remove back faces.
GLES20.glEnable(GLES20.GL_CULL_FACE);
 
// Enable depth testing
GLES20.glEnable(GLES20.GL_DEPTH_TEST);

作爲優化,您可以告訴OpenGL消除對象背面的三角形。當我們定義立方體時,我們還定義了每個三角形的三個點,並設置逆時針爲正面。當我們翻轉三角形時,我們將會看到三角形的背面,三角形的3個頂點順時針出現。你只能同時看到一個立方體的三個面,所以這個優化告訴OpenGL不要浪費時間繪製三角形的背面。

之後,當我們繪製透明對象時,我們可能希望將剔除變回,因爲這樣就可以看到對象的背面。

我們還啓用了深度測試。如果你總是從後到前按順序繪製物體,那麼深度測試並不是絕對必要的,但通過啓用它不僅不需要擔心繪製順序(儘管如果你先繪製更近的對象,渲染速度會更快),但是一些顯卡也將進行優化,通過花費更少的時間繪製將被繪製的像素來加速渲染

加載着色器程序的更改

因爲在OpenGL中加載着色器程序的步驟大致相同,所以這些步驟可以很容易地重構爲單獨的方法。 我們還添加了以下調用來檢索調試信息,以防編譯/鏈接失敗:

GLES20.glGetProgramInfoLog(programHandle);
GLES20.glGetShaderInfoLog(shaderHandle);

光源位置的頂點和片元着色器程序

有一個新的頂點和片元着色器程序專門用於在屏幕上繪製代表燈光當前位置的點:

    // Define a simple shader program for our point.
    final String pointVertexShader =
            "uniform mat4 u_MVPMatrix;      \n"
                    + "attribute vec4 a_Position;     \n"
                    + "void main()                    \n"
                    + "{                              \n"
                    + "   gl_Position = u_MVPMatrix   \n"
                    + "               * a_Position;   \n"
                    + "   gl_PointSize = 5.0;         \n"
                    + "}                              \n";

    final String pointFragmentShader =
            "precision mediump float;       \n"
                    + "void main()                    \n"
                    + "{                              \n"
                    + "   gl_FragColor = vec4(1.0,    \n"
                    + "   1.0, 1.0, 1.0);             \n"
                    + "}                              \n";

此着色器類似於第一課中的簡單着色器。 有一個新屬性gl_PointSize我們硬編碼到5.0; 這是輸出點大小(以像素爲單位)。 當我們使用GLES20.GL_POINTS作爲模式繪製點時使用它。 我們還將輸出顏色硬編碼爲白色。

進一步練習

  • 嘗試刪除“過渡飽和的保護”看會發生什麼
  • 光照方式存在缺陷。你能發現它是什麼嗎? 提示:我做環境光計算的方式的缺點是什麼,以及alpha會發生什麼?
  • 如果將gl_PointSize添加到正方體着色器並使用GL_POINTS繪製它會發生什麼?

可以從GitHub上的項目站點下載本課程的完整源代碼

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