第十六章
光 • 多重光源
承前章所學,吾人已悉 OpenGL 中之光照堂奧,舉凡 Phong 著色、材質、光照貼圖暨各類光源,皆已詳盡闡述。本章之旨,乃融會貫通前所學,以六光源齊發,營造一臻於完善之光照場景。吾人將模擬日暉,作定向光源;佐以四點光源,散佈於場景之中;復增一聚光燈,以擬手電之效。
欲於場景中運用多重光源,吾人宜將光照計算封裝於 GLSL 之「函式」(functions
)之中。蓋因光源種類繁多,各需殊異之運算,若悉數羅列於「主函式」(main
function)之內,程式碼恐將迅速趨於繁蕪,難以索解。
GLSL 中之函式,與 C 語言之函式無異,俱有函式名、回傳型別,且若函式未先於主函式之前宣告,則需於程式碼檔案之頂端宣告其原型。吾人將為每種光源,即定向光、點光源、聚光燈,各別創建函式。
當場景中運用多重光源之際,其常規做法如下:吾人設一單一顏色向量,以表片段之輸出色彩。每逢一光源,其對片段之貢獻,便疊加至此輸出顏色向量之上。是以,場景中之每一光源,皆將計算其個別之影響,並將其貢獻予最終之輸出色彩。其總體架構,約莫如下所示:
out vec4 FragColor;
void main()
{
// define an output color value
vec3 output = vec3(0.0);
// add the directional light's contribution to the output
output += someFunctionToCalculateDirectionalLight();
// do the same for all point lights
for(int i = 0; i < nr_of_point_lights; i++)
output += someFunctionToCalculatePointLight();
// and add others lights as well (like spotlights)
output += someFunctionToCalculateSpotLight();
FragColor = vec4(output, 1.0);
}
其實際程式碼或因實作而異,然其總體架構仍舊不變。吾人定義若干函式,各司其職,計算每光源之影響,並將所得之色彩增益至輸出色彩向量。舉例而言,若兩光源鄰近某片段,則其綜合貢獻將使該片段較單一光源照耀之下更為明亮。
定向光源
吾人欲於片段著色器中定義一函式,以計算定向光源對應片段之貢獻:此函式將接收若干參數,並回傳所計算之定向光照色彩。
首先,吾人需設定定向光源所需之最低變數。吾人可將此等變數儲存於名為 DirLight
之結構中,並將其定義為統一變數(uniform)。此結構之變數,應已於前章所示,想必諸君已然稔熟:
struct DirLight {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
uniform DirLight dirLight;
吾人可將此定向光源(dirLight)統一變數,傳遞予具備以下原型之函式:
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
正如C語言與C++之慣例,當吾人欲呼叫一函式(此處為main
函式內部),該函式務必於呼叫者之行數之前定義。然在此情境下,吾人更傾向於將函式定義於main
函式之後,故此項要求便不復存在。是以,吾人當於main
函式之上方,宣告函式之原型,一如C語言之作法。
足見此函式需一 DirLight 結構,並輔以計算所需之另兩個向量。若閣下已圓滿完成前一章節,則此函式之內容應不足為奇:
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
vec3 lightDir = normalize(-light.direction);
// diffuse shading
float diff = max(dot(normal, lightDir), 0.0);
// specular shading
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// combine results
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
return (ambient + diffuse + specular);
}
吾人基本上複製了前章之程式碼,並運用函式參數所賦予之向量,以計算定向光之貢獻向量。隨後,將所生之環境光、漫射光與鏡面反射光貢獻,彙總為單一色彩向量而回傳。
點光源(Point light)
吾人亦欲比照定向光源之例,定義一函式,以計算點光源對特定片段之貢獻,並納入其衰減效應。誠如定向光源,吾人將定義一結構,詳列點光源所需之諸般變數:
struct PointLight {
vec3 position;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
#define NR_POINT_LIGHTS 4
uniform PointLight pointLights[NR_POINT_LIGHTS];
可見吾人於 GLSL 中運用預處理指令,以定義場景中所需點光源之數量。繼而,吾人運用此NR_POINT_LIGHTS
常數,創建一PointLight
結構陣列。GLSL 中之陣列,一如 C 語言之陣列,可藉由方括號創建。當前吾人有四個PointLight
結構待以數據填充。
此點光源函式之原型如下:
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
此函式將其所需之所有數據作為引數,並回傳一 vec3
型別,以表示此特定點光源對片段之色彩貢獻。同樣地,透過巧妙之複製貼上,可得以下函式:
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);
// diffuse shading
float diff = max(dot(normal, lightDir), 0.0);
// specular shading
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// attenuation
float distance = length(light.position - fragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
// combine results
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
return (ambient + diffuse + specular);
}
將此功能抽象化為函式,其優點在於吾人可輕易計算多個點光源之照明,而無需重複程式碼。在main
函式中,吾人僅需創建一迴圈,遍歷點光源陣列,並為每個點光源呼叫CalcPointLight
函式。
總體匯集
如今吾人已為定向光源與點光源各自定義函式,可將其彙集於main
函式之中。
void main()
{
// properties
vec3 norm = normalize(Normal);
vec3 viewDir = normalize(viewPos - FragPos);
// phase 1: Directional lighting
vec3 result = CalcDirLight(dirLight, norm, viewDir);
// phase 2: Point lights
for(int i = 0; i < NR_POINT_LIGHTS; i++)
result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);
// phase 3: Spot light
//result += CalcSpotLight(spotLight, norm, FragPos, viewDir);
FragColor = vec4(result, 1.0);
}
每種光源皆將其貢獻增益於最終輸出色彩,直至所有光源處理完畢。所得色彩即包含場景中所有光源之綜合影響。吾人將CalcSpotLight
函式之定義留作讀者之練習。
此方法在各類光源函式中存在諸多重複計算(例如,計算反射向量、漫反射和鏡面反射項,以及採樣材質紋理),故仍有優化空間。
設定定向光源結構之統一變數應不甚陌生,然閣下或將疑惑如何設定點光源之統一變數,蓋因點光源之統一變數實乃PointLight
結構之陣列。此乃吾人此前未曾論及者。
幸運者,此並非過於複雜。設定結構陣列之統一變數,一如設定單一結構之統一變數,惟此次吾人亦需於查詢統一變數位置時,定義適當之索引:
lightingShader.setFloat("pointLights[0].constant", 1.0f);
此處吾人索引pointLights
陣列中之首個PointLight
結構,並於內部檢索其constant
變數之位置,並將其設為1.0
。
莫忘吾人亦需為四個點光源各自定義位置向量,故將其散佈於場景之中。吾人將定義另一glm::vec3
陣列,其中包含點光源之位置:
glm::vec3 pointLightPositions[] = {
glm::vec3( 0.7f, 0.2f, 2.0f),
glm::vec3( 2.3f, -3.3f, -4.0f),
glm::vec3(-4.0f, 2.0f, -12.0f),
glm::vec3( 0.0f, 0.0f, -3.0f)
};
隨後,吾人索引pointLights
陣列中對應之PointLight
結構,並將其position
屬性設為吾人方才定義之位置之一。此外,務必繪製四個光源立方體而非僅一個。只需為每個光源物體創建不同之模型矩陣,一如吾人處理容器之法。
若閣下亦使用手電筒,所有組合光源之結果約莫如下所示:
誠如所見,天空中似有某種全域光(如太陽),場景中散佈著四個光源,且從玩家視角可見手電筒之光。此景豈不甚為雅觀?
閣下可於此處尋得最終應用程式之完整原始碼:https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/6.multiple_lights/multiple_lights.cpp。
此圖所示之所有光源皆設定為吾人於前章所用之預設光照屬性,然若閣下調整此等數值,或可獲致頗為有趣之結果。藝術家與關卡設計師通常於大型編輯器中調整所有此等光照變數,以確保光照與環境相符。運用吾人簡樸之環境,閣下僅需調整光源屬性,便可創造出一些頗為引人入勝之視覺效果:
吾人亦更改了清屏顏色,以更好地反映光照效果。閣下可見,僅僅調整部分光照參數,便可創造出截然不同之氛圍。
至此,閣下應已對 OpenGL 中之光照有相當深入之理解。憑藉迄今所學,吾人已能創建引人入勝且視覺豐富之環境與氛圍。請嘗試調整所有不同之數值,以創建閣下專屬之氛圍。
- 1. 介绍
- 2. 开始 • 认识 OpenGL
- 3. 开始 • 创建一个窗口
- 4. 开始 • Hello, 窗口
- 5. 开始 • Hello, 三角形
- 6. 开始 • 著色器
- 7. 开始 • 紋理
- 8. 开始 • 轉換
- 9. 开始 • 座標系統
- 10. 开始 • 相機
- 11. 光 • 顏色
- 12. 光 • 基本光照
- 13. 光 • 材質
- 14. 光 • 光照貼圖
- 15. 光 • 光源
- 16. 光 • 多重光源
- 17. 模型載入 • Assimp
- 18. 模型載入 • Mesh
- 19. 模型載入 • Model
- 20. 高級 OpenGL • 深度測試
- 21. 高級 OpenGL • 模板測試
- 22. 高級 OpenGL • 混合
- 23. 高級 OpenGL • 面剔除
- 24. 高級 OpenGL • Framebuffers
- 25. 高級 OpenGL • Cubemaps
- 26. Advanced-OpenGL • Advanced-Data
- 27. Advanced-OpenGL • Advanced-GLSL
- 28. Advanced-OpenGL • Instancing
- 29. Advanced-OpenGL • Anti-Aliasing
- 30. 高級光照
- 31. 高級光照 • 伽馬矯正
- 32. 高級光照 • Shadow-Mapping
- 33. Advanced-Lighting • Point-Shadows
- 34. 高級光照 • 法線貼圖 (Normal Mapping)
- 35. 高級光照 • 視差貼圖(Parallax Mapping)
- 36. 高級光照 • HDR
- 37. 高級光照 • 輝光(Bloom)
- 38. 高級光照 • 延遲著色
- 39. 高級光照 • SSAO