原文: http://ogldev.atspace.co.uk/www/tutorial20/tutorial20.htmlcss
CSDN完整版專欄: http://blog.csdn.net/column/details/13062.htmlhtml
以前已經學習了三個主要的光照模型(環境光,漫射光和鏡面反射光),這三種模型都是基於平行光的。平行光僅僅是經過一個向量來表示,沒有光源起點,所以它不會隨着距離的增大而衰減(實際上沒有起點根本沒法定義光源和某個物體的距離)。算法
現在咱們再來看點光源類型,它有光源起點而且有衰減效果。距離光源越遠光線越弱。點光源的經典樣例是燈泡。燈泡在屋子裏可能效果不明顯,但是拿到室外就會明顯看出它的衰減效果了。注意以前平行光的方向是恆定的。但點光源光線的方向是變化的,四處擴散。數組
點光源想各個方向均勻照耀,所以點光源的方向要經過計算物體到點光源之間的向量獲得,這就是爲何要定義點光源的起點而不是它的方向。markdown
點光源光線慢慢變淡的的想象叫作‘衰減’。真實光線的衰減是依照平方反比定律的。也就是說光線的強度和離光源的距離的平方成反比。數學原理例如如下圖中的公式:數據結構
但3D圖形中這個公式計算的結果看上去效果並很差。好比:當距離很是近時,光的強度接近無窮大了。函數
另外,開發人員除了經過設置光的起始強度外沒法控制點光源的亮度,這樣就太受限制了。所以咱們加入了幾個新的因素到公式中使對其的控制更加靈活:post
咱們在分母上加入了三個光衰減的參數因子。一個常量參數,一個線性參數和一個指數參數。當將常量參數和線性參數設置爲零且指數參數設置爲1時,就和實際的物理公式是相應的了。也就是這個特殊狀況下在物理上是準確的。性能
當設置常量因子參數爲1時。調節另外兩個參數整體上就有比較好的衰減變化效果了。學習
常量參數的設置是要保證當距離爲0時光照強度達到最大(這個要在程序內進行配置),而後隨着距離的增大光照強度要慢慢減弱,因分母在慢慢變大。控制好線性參數因子和指數參數因子的變化,就可以實現想要的衰減效果。線性參數主要用於實現緩慢的衰減效果而指數因子可以控制光強度的迅速衰減。
現在總結計算點光源需要的步驟:
(lighting_technique.h:24)
struct BaseLight
{
Vector3f Color;
float AmbientIntensity;
float DiffuseIntensity;
};
.
.
.
struct PointLight : public BaseLight
{
Vector3f Position;
struct
{
float Constant;
float Linear;
float Exp;
} Attenuation;
}
平行光儘管和點光源不同。但它們仍然有很是多共同之處。它們共同的部分都放到了BaseLight結構體中,而點光源和平行光的結構體則繼承自BaseLight。平行光額外加入了方向屬性到它的類中,而點光源則加入了世界座標系中的位置變量和那三個衰減參數因子。
(lighting_technique.h:81)
void SetPointLights(unsigned int NumLights, const PointLight* pLights);
這個教程除了展現如何實現點光源。還展現如何使用多光源。一般僅僅存在一個平行光光源,也就是太陽光,另外可能還會有一些點光源(屋子裏的燈泡。地牢裏的火把等等)。
這個函數參數有一個點光源數據結構的數組和數組的長度。使用結構體的值來更新shader。
(lighting_technique.h:103)
struct {
GLuint Color;
GLuint AmbientIntensity;
GLuint DiffuseIntensity;
GLuint Position;
struct
{
GLuint Constant;
GLuint Linear;
GLuint Exp;
} Atten;
} m_pointLightsLocation[MAX_POINT_LIGHTS];
爲了支持多個點光源,shader需要包括一個和點光源結構體(僅僅在GLSL中)內容同樣的結構體數組。主要有兩種方法來更新shader中的結構體數組:
可以獲取每個數組元素中每個結構字段的位置(好比,一個數組假設有五個結構體,每個結構體四個字段,那就需要20個‘位置一致變量’),而後單獨設置每個元素中每個字段的值。
也可以僅僅獲取數組第一個元素每個字段的位置,而後用一個GL函數來保存元素中每個字段的屬性類型。好比,數組元素也就是一個結構體的第一個字段是一個float變量。第二個是一個integer變量。就可以在一次回調中使用一個float數組遍歷設置數組中每個結構體第一個字段的值,而後在第二次回調中使用一個int數組來設置每個結構體的第二個值。
第一種方法因爲要維護大量的位置一致變量所以很是浪費資源。但是會更加靈活。因爲你可以經過位置一致變量訪問更新數組中的不論什麼一個元素,不需要像另一種方法那樣先要轉換輸入的數據。
另一種方法不需要管理那麼多的位置一致變量。但是假設想要同一時候更新數組中的幾個元素的話,同一時候用戶傳入的又是一個結果體數組(像SetPointLights()),你就要先將這個結構體數組轉換成多個字段的數組結構,因爲結構體中每個位置的字段數據都要使用一個同類型的數組來更新。當使用結構體數組時,在數組中兩個連續元素(結構體)中的同一個字段之間存在內存間隔(被其它字段間隔開了,咱們是想要同一個字段的連續字段數組)。需要將它們收集到它們本身的同類型數組中。本教程中,咱們將使用第一種方法。最好兩個都實現一下,看你認爲哪個方法更好用。
MAX_POINT_LIGHTS是一個常量,用於限制可以使用的點光源的最大數量,而且必須和着色器中的相應值同步一致。默認值爲2。當你添加應用中光的數量,隨着光源的添加會發現性能愈來愈差。這個問題可以使用一種稱爲「延遲着色」的技術來優化解決,這個後面再探討。
(lighting.fs:46)
vec4 CalcLightInternal(BaseLight Light, vec3 LightDirection, vec3 Normal)
{
vec4 AmbientColor = vec4(Light.Color, 1.0f) * Light.AmbientIntensity;
float DiffuseFactor = dot(Normal, -LightDirection);
vec4 DiffuseColor = vec4(0, 0, 0, 0);
vec4 SpecularColor = vec4(0, 0, 0, 0);
if (DiffuseFactor > 0) {
DiffuseColor = vec4(Light.Color * Light.DiffuseIntensity * DiffuseFactor, 1.0f);
vec3 VertexToEye = normalize(gEyeWorldPos - WorldPos0);
vec3 LightReflect = normalize(reflect(LightDirection, Normal));
float SpecularFactor = dot(VertexToEye, LightReflect);
if (SpecularFactor > 0) {
SpecularFactor = pow(SpecularFactor, gSpecularPower);
SpecularColor = vec4(Light.Color * gMatSpecularIntensity * SpecularFactor, 1.0f);
}
}
return (AmbientColor + DiffuseColor + SpecularColor);
}
這裏在平行光和點光源之間實現很是多着色器代碼的共享就不算什麼新技術了。大多數算法是一樣的。不一樣的是,咱們僅僅需要考慮點光源的衰減因素。 此外,針對平行光,光的方向是由應用提供的。而對點光源,需要計算每個像素的光的方向。
上面的函數封裝了兩種光類型之間的共用部分。 BaseLight結構體包括光強度和顏色。
LightDirection是額外單獨提供的,緣由上面剛剛已經提到。 另外還提供了頂點法線,因爲咱們在進入片斷着色器時要對其進行一次單位化處理。而後在每次調用此函數時使用它。
(lighting.fs:70)
vec4 CalcDirectionalLight(vec3 Normal)
{
return CalcLightInternal(gDirectionalLight.Base, gDirectionalLight.Direction, Normal);
}
有了公共的封裝函數,定義函數簡單的包裝調用一下就可以計算出平行光了,參數多數來自全局變量。
(lighting.fs:75)
vec4 CalcPointLight(int Index, vec3 Normal)
{
vec3 LightDirection = WorldPos0 - gPointLights[Index].Position;
float Distance = length(LightDirection);
LightDirection = normalize(LightDirection);
vec4 Color = CalcLightInternal(gPointLights[Index].Base, LightDirection, Normal);
float Attenuation = gPointLights[Index].Atten.Constant +
gPointLights[Index].Atten.Linear * Distance +
gPointLights[Index].Atten.Exp * Distance * Distance;
return Color / Attenuation;
}
計算點光比定向光要複雜一點。每個點光源的配置都要調用這個函數。所以它將光的索引做爲參數,在全局點光源數組中找到相應的點光源。
它依據光源位置(由應用程序在世界空間中提供)和由頂點着色器傳遞過來的頂點世界空間位置來計算光源方向向量。使用內置函數length()計算從點光源到每個像素的距離。 一旦咱們有了這個距離。就可以對光的方向向量進行單位化處理。
注意,CalcLightInternal()是需要一個單位化的光方向向量的。平行光的單位化由LightingTechnique類來負責。 咱們使用CalcInternalLight()函數得到顏色值。並使用咱們以前獲得的距離來計算光的衰減。終於點光源的顏色是經過將顏色和衰減值相除計算獲得的。
(lighting.fs:89)
void main()
{
vec3 Normal = normalize(Normal0);
vec4 TotalLight = CalcDirectionalLight(Normal);
for (int i = 0 ; i < gNumPointLights ; i++) {
TotalLight += CalcPointLight(i, Normal);
}
FragColor = texture2D(gSampler, TexCoord0.xy) * TotalLight;
}
有了前面的基礎。片斷着色器方面就變得很是easy了。簡單地將頂點法線單位化。而後將所有類型光的效果疊加在一塊兒。結果再乘以採樣的顏色,就獲得終於的像素顏色了。
(lighting_technique.cpp:279)
void LightingTechnique::SetPointLights(unsigned int NumLights, const PointLight* pLights)
{
glUniform1i(m_numPointLightsLocation, NumLights);
for (unsigned int i = 0 ; i < NumLights ; i++) {
glUniform3f(m_pointLightsLocation[i].Color, pLights[i].Color.x, pLights[i].Color.y, pLights[i].Color.z);
glUniform1f(m_pointLightsLocation[i].AmbientIntensity, pLights[i].AmbientIntensity);
glUniform1f(m_pointLightsLocation[i].DiffuseIntensity, pLights[i].DiffuseIntensity);
glUniform3f(m_pointLightsLocation[i].Position, pLights[i].Position.x, pLights[i].Position.y, pLights[i].Position.z);
glUniform1f(m_pointLightsLocation[i].Atten.Constant, pLights[i].Attenuation.Constant);
glUniform1f(m_pointLightsLocation[i].Atten.Linear, pLights[i].Attenuation.Linear);
glUniform1f(m_pointLightsLocation[i].Atten.Exp, pLights[i].Attenuation.Exp);
}
}
此函數經過迭代遍歷數組元素並依次傳遞每個元素的屬性值。而後使用點光源的值更新着色器。 這是前面所說的「方法1」。
本教程的Demo顯示兩個點光源在一個場景區域中互相追逐。
一個光源基於餘弦函數,而還有一個光源基於正弦函數。該場景區域是由兩個三角形組成的很是easy的四邊形平面,法線是一個垂直的向量。