寫在前面
上一節光照中使用材質和lighting maps介紹了使用材質屬性和lighting maps使物體的光照效果能反映物體的材料特性,看起來更逼真。在前面的章節中使用的實際上都是一個點光源,本節將學習其他幾種光源類型,以及在場景中使用多個光源
通過本節可以瞭解到
光源類型
在前面章節中,我們通過位置和成分分量大小來指定光源屬性,這個光源實際上是一個點光源。除了點光源外,還包括方向光源,聚光燈光源等其他類型的光源,他們的特點如下圖所示(來自Simple
Lighting):
使用不同的光源主要是爲了模擬現實環境中不同類型的光,使場景光照效果更能滿足需求。每種類型的光源各有其特點,下面予以介紹。
方向光源
方向光源的特點就是光的方向幾乎都平行,只有一個方向,這是爲了模擬光源在無限遠處的情景,例如太陽光。方向光源一般不考慮光的衰減,它與光源具體位置無關,我們只需要爲它指定方向即可。注意一般我們指定方向光源的方向時,習慣從光源指向物體,而在計算光照時,又需要從物體指向光源的方向,因此需要做一個翻轉。指定方向光源的結構體在着色器中定義爲:
struct DirLightAttr
{
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
在計算光照時,我們不必利用物體的位置和光源位置計算光源方向了,
vec3 lightDir = normalize(light.position - FragPos);
直接使用這個direction即可:
vec3 lightDir = normalize(-light.direction);
在着色器中計算光照的效果與上一節的光照計算是相同的:
void main()
{
vec3 ambient = light.ambient * vec3(texture(material.diffuseMap, TextCoord));
vec3 lightDir = normalize(-light.direction);
vec3 normal = normalize(FragNormal);
float diffFactor = max(dot(lightDir, normal), 0.0);
vec3 diffuse = diffFactor * light.diffuse * vec3(texture(material.diffuseMap, TextCoord));
float specularStrength = 0.5f;
vec3 reflectDir = normalize(reflect(-lightDir, normal));
vec3 viewDir = normalize(viewPos - FragPos);
float specFactor = pow(max(dot(reflectDir, viewDir), 0.0), material.shininess);
vec3 specular = specFactor * light.specular * vec3(texture(material.specularMap, TextCoord));
vec3 result = ambient + diffuse + specular;
color = vec4(result , 1.0f);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
另外還需要在主程序中指定光源的方向如下:
GLint lightDirLoc = glGetUniformLocation(shader.programId, "light.direction")
glm::vec3 lampDir(0.5f, 0.8f, 0.0f)
glUniform3f(lightDirLoc, lampDir.x, lampDir.y, lampDir.z)
使用方向光源的效果就是,場景中無論遠近,物體得到的光照都一樣,光從同一個平行的方向射向物體表面,效果如下圖所示:
點光源
在前面兩節使用的光源都是一個簡單的點光源,場景中物體不管離光源位置遠近得到的光照強度都相同,這一點與實際不相符合。實際中的點光源向各個方向發射光,但是物體與光源的距離d增大時光照的強度將會減弱。我們需要模擬這個特點來是點光源更加逼真。光照強度的衰減係數Fatt與距離d之間的關係如何確定呢?
如果簡單的使用線性函數,距離稍微遠點的物體光照強度減小得太過於明顯,不符合實際情況,因此一般要考慮使用二次函數。可以定義光照強度的衰減係數Fatt與距離d之間的關係如下式:
Fatt=1.0Kc+Kl∗d+Kq∗d2
其中Kc表示常係數,當d=0時,Fatt=1表示沒有衰減,這時光照強度最大;Kl表示線性衰減係數,Kq表示二次衰減係數。使用上述公式計算光照的衰減時,大致的走勢如下圖所示(來自www.learnopengl.com):
可以看出當距離較近時光照強度較大,當距離超過一定範圍後光照強度就很弱了,光照強度的較小不是直線型的,而是曲線型的,這樣更符合實際情形。
調整上述衰減係數以模擬逼真的光照效果,需要仔細玩耍這些參數,是一件需要經驗的工作
在主程序中設置衰減係數如下:
GLint attConstant = glGetUniformLocation(shader.programId, "light.constant");
GLint attLinear = glGetUniformLocation(shader.programId, "light.linear");
GLint attQuadratic = glGetUniformLocation(shader.programId, "light.quadratic");
glUniform1f(attConstant, 1.0f);
glUniform1f(attLinear, 0.09f);
glUniform1f(attQuadratic, 0.032f);
在着色器中對應的點光源結構體更新爲:
struct PointLightAttr
{
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
計算光照強度後乘以使用衰減係數:
float distance = length(light.position - FragPos); // 在世界座標系中計算距離
float attenuation = 1.0f / (light.constant
+ light.linear * distance
+ light.quadratic * distance * distance);
vec3 result = (ambient + diffuse + specular) * attenuation;
color = vec4(result , 1.0f);
使用有衰減的點光源效果如下圖所示:
這裏我們看到紅色箭頭指向的物體隨着與光源距離不同光照有明暗不同的效果。
聚光燈光源
聚光燈光源的特點是光只在一個指定的範圍內發散,如下圖所示(來自www.learnopengl.com):
注意指定聚光燈指定了3個方面:
-
SpotDir 聚光燈的燈軸的方向
-
LightPos 聚光燈的位置
-
Cutoff 聚光燈的張角 即圖中的ϕ
在計算聚光燈的光照效果時需要計算的量包括:
-
lightDir 物體的位置和光源位置之差構成的光照射方向
-
θ是lightDir與SpotDir之間的夾角
聚光燈的特點是,當夾角θ <= ϕ時物體接受到光照,當θ > ϕ物體落在聚光燈的照明範圍外,將得不到光照。
在着色器中定義聚光燈的結構體爲:
struct SpotLightAttr
{
vec3 position;
vec3 direction;
float cutoff;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
注意聚光燈在傳遞張角時,使用了一個技巧,即傳遞夾角的餘弦值而不是角度值。對於cos函數,在[0,π2]時函數遞減,如下圖左邊部分所示:
那麼當θ <= ϕ時,有cos(θ)>=cos(ϕ),這一點在着色器中將會利用到。
在主程序中設置參數的方法同上述方向光源和點光源一樣,這裏不再贅述。在着色器中計算聚光燈效果的實現思路爲:
void main()
{
vec3 lightDir = normalize(light.position - FragPos);
float theta = dot(lightDir,normalize(-light.direction));
if(theta > light.cutoff)
{
}
else
{
}
}
其中計算漫反射和鏡面反射同點光源的計算是一樣的,也需要考慮光照強度衰減的因素。
如果將聚光燈的位置定義爲觀察者所在位置,將聚光燈的光軸方向定義爲觀察者的觀察正向,那麼就可以實現手電筒的效果。當觀察者在場景中移動時,手電筒發出光位置也隨着改變,如下圖所示:
改進的手電筒實現
上述十年的手電筒效果,存在一個缺陷,就是當物體超過手電筒這個聚光燈模型的張角時,場景立馬變暗了,這個與實際情形不符合。實際拿着手電筒時,物體不再張角範圍內時是逐漸變暗的,而這裏實現的手電筒的邊緣部分帶有很明顯的變暗的感覺。可以爲聚光燈模型指定兩個張角,ϕ用於內張角餘弦值,r用於外張角餘弦值,定義光照強度爲:
-
0.0f 當θ 落在外部張角之外時,沒有光照
-
[0.0f,1.0f]之間
當θ 落在內外張角之間時
-
1.0f 當θ 位於內部張角之內時
我們需要定義一個函數來實現這個光照強度的計算,如下圖中右半部分所示:
並結合OpenGL提供的clamp函數來實現,clamp函數定義如下:
API genType clamp( genType x,
genType minVal,
genType maxVal);
這個函數將值x截斷到minVal和maxVal之間。
這裏我們使用一個形如如下形式的函數:
f1=θ−γϵ
其中:
θ爲lightDir與SpotDir的夾角的餘弦,
r表示聚光燈的外張角的餘弦,
ϵ=ϕ−r表示內外張角的餘弦之差。
可以驗證上述
f1函數滿足圖中
f函數的要求,則我們可以在着色器中實現改進的手電筒模型爲:
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutoff - light.outerCutoff;
float intensity = clamp((theta - light.outerCutoff) / epsilon, 0.0, 1.0);
diffuse *= intensity;
specular *= intensity;
改進時,對漫反射光和鏡面光成分乘以通過內外張角計算出來的強度係數,其餘部分不變。通過增加的這個intensity來計算光照後,手電筒效果如下:
從上圖可以看出,改進之後從內張角到外張角之間這部分逐漸變暗,這樣的效果更佳符合實際手電筒的效果。
使用多個光源
上面學習了方向光源、點光源以及聚光燈光源,我們可以將他們應用到一個場景中。在着色器中,定義計算場景中各個不同類型光照效果的函數如下:
vec3 calculateDirLight(DirLightAttr light, vec3 fragNormal, vec3 fragPos,vec3 viewPos);
vec3 calculatePointLight(PointLightAttr light, vec3 fragNormal, vec3 fragPos,vec3 viewPos);
vec3 calculateSpotLight(SpotLightAttr light,vec3 fragNormal, vec3 fragPos, vec3 viewPos);
定義多個光源:
uniform DirLightAttr dirLight;
#define POINT_LIGHT_NUM 4
uniform PointLightAttr pointLights[POINT_LIGHT_NUM];
uniform SpotLightAttr spotLight;
則計算總的光照效果的函數實現爲:
void main()
{
vec3 result = calculateDirLight(
dirLight, FragNormal,
FragPos, viewPos);
for(int i = 0; i < POINT_LIGHT_NUM; ++i)
{
result += calculatePointLight(
pointLights[i],FragNormal,
FragPos, viewPos);
}
result += calculateSpotLight(
spotLight, FragNormal,
FragPos, viewPos);
color = vec4(result , 1.0f);
}
實現函數calculatexxxLight的方法同上面講到的一樣,是對相關類型光源光照計算的一個函數封裝,這裏不再贅述。需要注意的是點光源包含一個數組,設置數組中每個光源時,需要在主程序中使用數組的索引方式,例如:
GLint lightAmbientLoc = glGetUniformLocation(
shader.programId,"pointLights[0].ambient");
glUniform3f(lightAmbientLoc, 0.0f, 0.1f, 0.4f);
根據需要調整各個多個光源的參數值後,實現的一個藍色調效果爲:
最後的說明
本節學習了三種光源類型,以及如何實現光照的計算,並在最後將多個類型的光源應用到了同一個場景中。到目前爲止,我們已經學習了基本的光照,能夠實現一些理想的效果了,後面還會繼續學習光照的高級技巧。接下來,我們打算學習加載模型的方法,使場景中的物體更豐富