OpenGL Android課程二:環境光和漫射光
翻譯文
原文標題:OpenGL Android Lesson One: Getting Started
原文連結: www.learnopengles.com/android-les…
歡迎來到第二課,我們將學習如何使用 著色器實現 朗伯反射( Lambertian reflectance ) ,也稱為標準漫射照明。 在OpengGL ES 2,我們需要實現我們自己的照明演算法, 因此我們要學會數學如何工作以及如何應用到我們的場景中。 |
![]() |
閱讀本文前提條件
本系列的每節課都以前面的課程為基礎。在開始前,請看第一課,因為本課程將以此為基礎概念介紹。
什麼是光
沒錯!一個沒有光的世界是昏暗的。沒有[光],我們甚至不能感知世界或我們周圍的物體,除了聲音和觸控等其他感官。 光向我們展示了物體是明亮還是昏暗,是遠還是近,它的角度是什麼。
在現實世界,我們所感知的光實際是數萬億微小粒子的聚集,稱為光子。它從光源飛出,反彈數千或數百萬次,最終到達我們的眼鏡我們稱之為光。
我們如何通過計算機圖形模擬光的影響? 有兩種流行的方法:光線追蹤和光柵化 光線跟蹤的工作原理是通過數學計算跟蹤實際光線並檢視它們的最終位置。該技術可以得到非常精準和逼真的結果,但缺點是模擬所有這些光線的計算成本非常高,並且通常對於實時渲染來說太慢了。由於這個限制,大多數實時圖形計算使用光柵化,它通過近似值模擬光照。鑑於當前遊戲的真實性,光柵化看起來非常好,即使在手機上也可以快速實現實時圖形。OpengGL ES主要是一個光柵化庫,因此我們主要關注這個。
不同種類的光
事實證明,我們可以抽象出光的工作方式,並提出三種基本的光照方式
![]() |
環境光 這是基本的照明水平,似乎遍佈整個場景。它似乎不是來自任何 光源的光,因為它在到達你之前已經反彈了很多次。這種型別的光 在戶外的陰天可以體驗,或者在戶內作為許多不同光源的積累影響。 我們可以為物體或場景設定一個基本的亮度,而不是為所有的 光單獨計算。 |
![]() 的例子 |
漫射照明 這是直接從一個物體上跳彈後到達您眼睛中的光,物體的亮度 隨著它與照明的角度而變化,面向燈光的方向比其他角度更加明亮 此外,無論我們相對於物體的角度怎樣,我們都覺得物體是相同的 亮度,這也被稱為Lambert的餘弦定律。漫射照明或朗伯反射率在 日常生活中很常見,您可以在室內燈光照明的白牆上輕鬆看到。 |
![]() |
鏡面照明 與漫射照明不同,當我們相對於物體移動時,鏡面光照也會 發生改變。這給物體帶來“光澤”,並且可以在“更光滑”的表面 上看到,例如玻璃和其他有光澤的物體。 |
模擬光
正如3D場景中的3中主要型別的光照一樣,還有三種主要型別的光源:定向光源,點光源,聚光燈,這些也可以在日常生活中輕鬆看到。
![]() |
定向光源 定向光照通常來自於一個很遠的光源,它可以均勻的照亮整個 場景達到相同的亮度。這種光源是最簡單的型別,無論您處在 場景哪裡,光照都具有相同的強度和方向。 |
![]() |
點光源 點光源可以新增到場景中,以提供更多樣化和逼真的照明。 點光的照射隨著距離而下降,並且它的光線在所有方向上 向外傳播,光源位於中心。 |
![]() |
聚光燈 除了具有點光源的特性外,聚光燈也有光哀減的方向, 通常呈錐形。 |
數學
本節課,我們來看看來自一個點光源的環境照明和漫射照明。
環境照明
環境照明其實是間接漫射照明,但它也可以被認為是遍佈整個場景的低階光。如果我們這麼想,那麼它將非常好計算:
// 最終顏色 = 材質顏色 * 環境光顏色 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} 複製程式碼
物體的最終顏色將是暗紅色,如果您有一個被昏暗的白光照明的紅色物體,那麼這就是您的預期。基本的環境光真的沒有比這更多的了,除非您想加入更先進的照明技術,如光能傳遞。
漫射照明-點光源
對於漫射照明,我們需要新增哀減和光源位置。光源位置將用來計算光線和表面的角度,它將影響表面的整體光照水平。它還將用於計算光源到表面的距離,這決定了光在這個點上的強度。
第一步:計算朗伯因子(lambert factor)
我們最重要的是需要弄清楚表面和光線之間的角度。面向光直射的表面因該全強度照射,而傾斜的表面因該得到較少的照射,比較合適的計算方式是使用Lambert的餘弦定律。如果我們有兩個向量,一個是從光到表面上的一個點,第二個是表面的法線(如果表面是平面,則表面法線是指向上或垂直於該表面的向量),然後我們可以通過對每個向量進行歸一化來計算餘弦,使其長度為1,然後通過計算兩個向量的點積(數量積)。這個操作可以由OpenGL ES 2輕鬆完成。
我們稱這位朗伯因子,它的取值範圍在0~1之間
// 光線向量 = 光源位置 - 物體位置 light vector = light position - object position // 餘弦 = 物體法線和歸一化後的光線向量的點積 cosine = dot product(object normal, normalize(light vector)) // 朗伯因子 = 取餘弦和0中最大的 lambert factor = max(cosine, 0) 複製程式碼
首先我們通過光源位置減去物體位置得到光線向量,然後我們通過物體法線和光向量的點積得到餘弦。我們標準化光向量,這意味著改變它的長度,長度為1,這個物體的法線長度也是1,兩個歸一化向量的點積得到他們之間的餘弦。因為點積的取值範圍是-1~1,所以我們將其限制到0~1。
這兒有個處在原點的平面,其表面法線指向天空的例子。
光的位置在{0, 10, -10},我們想要計算在原點的光。
// 光線向量 light vector = {0, 10, -10} - {0, 0, 0} = {0, 10, -10} // 物體法線 object normal = {0, 1, 0} 複製程式碼
簡潔的說,如果們沿著光線向量走,我們到達光源的位置。為了歸一化向量,我們將每個分量除以向量長度:
// 光線向量長度 = 平方根(0*0 + 10*10 + (-10 * -10)) = 平方根(200) = 14.14 light vector length = square root(0*0 + 10*10 + (-10 * -10)) = square root(200) = 14.14 // 歸一化光線向量 normalize 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.707 複製程式碼
最後我們限制範圍:
// 朗伯因子 lambert factor = max(0.707, 0) = 0.707 複製程式碼
OpenGL ES 2的著色器語言內建了對其中一些函式的支援,因此我們不需要手動完成所有數學運算,但它仍然有助於理解正在發生的事情。
第二步:計算哀減係數
接下來,我們需要計算哀減。來自光源的實際光哀減遵循反平方定律
也可以這樣表示:
// 亮度 = 1 / 距離的平方 luminosity = 1 / (distance * distance) 複製程式碼
回到我們的列子,因為我們有光線長度為14.14,這兒我們最終的亮度:
luminosity = 1 / (14.14 * 14.14) = 1 / 200 = 0.005 複製程式碼
正如您所見,反平方定律會導致距離的強烈哀減。這就是點光源的光在現實世界中的作用,但是由於我們圖形展示範圍有限,控制這個哀減係數是非常有用的,因此我們仍然能獲得逼真的照明而不會讓其看起來很昏暗。
第三步:計算最終顏色
現在我們知道了餘弦和哀減度,我們可以計算我們最終的亮度:
// 最終顏色 = 材質顏色 * (光的顏色 * 朗伯因子 * 亮度) 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} 複製程式碼
回顧一下,對於漫射照明,我們需要使用表面和光線之間的角度以及距離,用來計算最終的整體漫射亮度。
以下是步驟:
// 第一步 light vector = light position - object position cosine = dot product(object normal, normalize(light vector)) lambert factor = mac(cosine, 0) // 第二步 luminosity = 1 / (distance * distance) // 第三步 final color = material color * (light color * lambert factor * luminosity) 複製程式碼
將這一切放到OpenGL ES 2著色器中
頂點著色器
final String vertexShader = "uniform mat4 u_MVPMatrix;\n" + // 一個表示組合model、view、projection矩陣的常量 "uniform mat4 u_MVMatrix;\n" + // 一個表示組合model、view矩陣的常量 "uniform vec3 u_LightPos;\n" + // 光源在眼睛空間(相對於相機視角)的位置 "attribute vec4 a_Position;\n" + // 我們將要傳入的每個頂點的位置資訊 "attribute vec4 a_Color;\n" + // 我們將要傳入的每個頂點的顏色資訊 "attribute vec3 a_Normal;\n" + // 我們將要傳入的每個頂點的法線資訊 "varying vec4 v_Color;\n" + // 這將被傳入片段著色器 "void main()\n" + // 頂點著色器入口 "{\n" + // 將頂點轉換成眼睛空間(相對於相機視角) "vec3 modelViewVertex = vec3(u_MVMatrix * a_Position);\n" + // 將法線的方向轉換成眼睛空間(相對於相機視角) "vec3 modelViewNormal = vec3(u_MVMatrix * vec4(a_Normal, 0.0));\n" + // 將用於哀減 "float distance = length(u_LightPos - modelViewVertex);\n" + // 獲取從光源到頂點方向的光線向量 "vec3 lightVector = normalize(u_LightPos - modelViewVertex);\n" + // 計算光線向量和頂點法線的點積,如果法線和光線向量指向相同的方向,那麼它將獲得最大的照明 "float diffuse = max(dot(modelViewNormal, lightVector), 0.1);\n" + // 根據距離哀減光線 "diffuse = diffuse * (1.0 / (1.0 + (0.25 * distance * distance)));\n" + // 將顏色乘以亮度,它將被插入三角形中 "v_Color = a_Color * diffuse;\n" + // gl_Position是一個特殊的變數用來儲存最終的位置 // 將頂點乘以矩陣得到標準化螢幕座標的最終點 "gl_Position = u_MVPMatrix * a_Position;\n" + "}\n"; 複製程式碼
這裡有相當多的事情要做。我們在第一課講到過我們要有一個model/view/projection的組合矩陣,但是我們還要添加了一個model/view矩陣。為什麼?因為我們將需要這個矩陣去計算光源位置到當前頂點位置之間的距離。對於漫射照明,無論您使用世界空間(model矩陣)或眼睛空間(model/view矩陣)只要你能計算出合適的距離和角度實際上都沒有問題。
我們傳入頂點的顏色和位置資訊,以及它的法線。我們會將最終的顏色傳入片段著色器,它將在頂點之間插值,這也被稱為Gouraud著色法。
讓我們來看看著色器每一部分的意義:
// 將頂點轉換成眼睛空間(相對於相機視角) "vec3 modelViewVertex = vec3(u_MVMatrix * a_Position);\n" 複製程式碼
因為我們是在眼睛空間觀察光源位置,我們轉換當前的頂點位置到眼睛空間的座標系中,因此我們能計算出對應的距離和角度。
// 將法線的方向轉換成眼睛空間(相對於相機視角) "vec3 modelViewNormal = vec3(u_MVMatrix * vec4(a_Normal, 0.0));\n" + 複製程式碼
我們也需要轉換法線的方向。這裡我們只是想上面位置一樣做了個常規乘法,但是如果model或view矩陣做過旋轉或傾斜,那麼將不能工作:我們實際上需要通過將法線乘以原始矩陣的反轉來消除傾斜或縮放的影響。 這個網站很好的解釋了為什麼我們必須這麼做
// 將用於哀減 "float distance = length(u_LightPos - modelViewVertex);\n" 複製程式碼
如前面數學部分所示,我們需要這個距離去計算哀減係數
// 獲取從光源到頂點方向的光線向量 "vec3 lightVector = normalize(u_LightPos - modelViewVertex);\n" 複製程式碼
我們也需要光線向量去計算朗伯反射因子
// 計算光線向量和頂點法線的點積,如果法線和光線向量指向相同的方向,那麼它將獲得最大的照明 "float diffuse = max(dot(modelViewNormal, lightVector), 0.1);\n" 複製程式碼
這與上面的數學部分相同,只是在OpenGL ES 2著色器中完成。後面的0.1是一種非常便宜的環境照明方式(最小值將被限制在0.1)。
// 根據距離哀減光線 "diffuse = diffuse * (1.0 / (1.0 + (0.25 * distance * distance)));\n" 複製程式碼
這裡和上面的數學部分略有不同。我們將距離的平方縮放0.25以抑制衰減的效應,並且我們還將修改的距離加1,這樣當光源非常接近物體時我們就不會過飽和(否則,當距離小於1時,該等式實際上回照亮光源而不是哀減它)。
// 將顏色乘以亮度,它將被插入三角形中 "v_Color = a_Color * diffuse;\n" + // gl_Position是一個特殊的變數用來儲存最終的位置 // 將頂點乘以矩陣得到標準化螢幕座標的最終點 "gl_Position = u_MVPMatrix * a_Position;\n" 複製程式碼
當我們有了最終的光色,我們將它乘以頂點的顏色得到最終輸出的顏色,然後我們將這個頂點的位置投影到螢幕上。
畫素著色器
final String fragmentShader = "precision mediump float;\n" + // 我們將預設精度設定為中等,我們不需要片段著色器中的高精度 "varying vec4 v_Color;\n" + // 這是從三角形每個片段內插的頂點著色器的顏色 "void main()\n" + // 片段著色器入口 "{\n" + "gl_FragColor = v_Color;\n" + // 直接將顏色傳遞 "}\n"; 複製程式碼
因為我們是在每個頂點的基礎上計算光,我們的片段著色器和上節課一樣,我們所做的是將顏色直接傳過去。在下節課中,我們將學習每畫素照明。
每頂點照明和每畫素照明
這節課我們的關注點在實現每頂點照明。對於具有光滑表面的物體(如地形),或具有許多三角形的物體的漫反射,這通常是足夠了。然而,當您的物體沒有包含許多頂點時(例如我們的在這個案例中的正方體),或者有尖角,頂點光照可能會導致偽影,因為亮度在多邊形上線性插值;當鏡面高光新增到影象時,這些偽影也會變得更加明顯。更多關於Gouraud著色法的Wiki文章
正方體的構造
在第一課中,我們將位置和顏色屬性打包到一個數組中,但是OpengGL ES 2也允許讓我們將屬性單獨存放:
//X, Y, Z final float[] cubePositionData = { // 在OpenGL,逆時針繞組(下面的點事逆時針順序)是預設的。 // 這意味著當我們在觀察一個三角形時,如果這些電視逆時針的,那麼我們正在看"前面",如果不是我們則正在看背面 // OpenGL有一個優化,所有背面的三角形都會被剔除,因為它們通常代表一個物體的背面,無論如何都不可見 // 正面 -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 = { // 正面紅色 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 flag
我們還使用了 glEnable()
呼叫啟用了剔除和深度緩衝:
// 使用剔除去掉背面 GLES20.glEnable(GLES20.GL_CULL_FACE); // 啟用深度測試 GLES20.glEnable(GLES20.GL_DEPTH_TEST); 複製程式碼
作為優化,您可以告訴OpenGL剔除物體背面的三角形。當我們定義正方體時,我們還定義了每個三角形的三個點,以便當我們在檢視正面的時候是逆時針的。當我們翻轉三角形以便我們到背面時,這些點將會順時針展示。 您只能同時看到一個正方體的三個面,所以這個優化告訴OpenGL不要浪費時間去繪製背面的三角形。
之後當我們繪製透明的物體時,我們希望關閉剔除,然後物體背面將會變得可見。
我們還開啟了深度測試。如果你總是從後面向前面繪製物體,那麼深度測試絕非必要,但是通過啟用它您不僅不需要擔心繪製順序(儘管如果你先畫最近的物體渲染會更快),一些顯示卡也將進行優化,通過花費更少的時間繪製畫素來加速渲染。
載入著色器程式的修改
因為在OpenGL中載入著色器程式的步驟大致相同,這些步驟可以很容易的重構為一個單獨的方法。我們還添加了以下呼叫來檢索除錯資訊,以防編譯/連結失敗:
GLES20.glGetProgramInfoLog(programHandle); GLES20.glGetShaderInfoLog(shaderHandle); 複製程式碼
光點的頂點和著色程式
這個新的頂點和著色器程式繪製在螢幕上代表當前光源的位置:
// 定義一個簡單的著色程式 final String pointVertexShader = "uniform mat4 u_MVPMatrix;\n" + "attribute vec4 a_Position;\n" + "void main()\n" + "{\n" + "gl_Position = u_MVPMatrix * 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, 1.0, 1.0, 1.0) \n" + "}\n"; 複製程式碼
這個著色器類似於第一課的簡單著色器,這裡有個新的成員 gl_PointSize
,直接固定它的值為5.0,這是點的畫素尺寸。當我們使用 GLES20.GL_POINTS
模式繪製這個點的時候它會被使用。我們也直接設定了它的顯示顏色為白色。
進一步練習
- 嘗試刪除“過渡飽和”看會發生什麼
- 這裡的照明方式存在缺陷,你能發現是什麼嗎?提示:我們做環境照明的方式的缺點是什麼,以及alpha會放生什麼?
- 如果將
gl_PointSize
新增到正方體著色器並使用GL_POINTS
繪製它會發生什麼?
進一步閱讀
在編寫本教程時,上面的進一步閱讀部分對我來說是非常寶貴的資源,因此我強烈建議您閱讀它們以獲得更多的資訊和解釋。