1. 程式人生 > >【GLSL教程】(七)逐畫素的光照

【GLSL教程】(七)逐畫素的光照

逐畫素的方向光(Directional Light per Pixel)
這一節將把前面的shader程式碼改為逐畫素計算的方向光。我們需要將工作按照兩個shader拆分,以確定哪些是需要逐畫素操作的。
首先看看每個頂點接收到的資訊:
•法線
•半向量
•光源方向
我們需要將法線變換到視點空間然後歸一化。我們還需要將半向量和光源方向也歸一化,不過它們已經位於視點空間中了。這些歸一化之後的向量會進行插值,然後送入片斷shader,所以需要宣告易變變數儲存這些向量。
我們也可以在頂點shader中完成一些與光和材質相關的計算,這樣可以幫助平衡頂點shader和片斷shader的負載。
頂點shader程式碼可以寫成如下形式:
  1. varying vec4 diffuse,ambient;
  2. varying vec3 normal,lightDir,halfVector;
  3. void main()
  4. {
  5. /* first transform the normal into eye space and
  6. normalize the result */
  7. normal = normalize(gl_NormalMatrix * gl_Normal);
  8. /* now normalize the light's direction. Note that
  9. according to the OpenGL specification, the light
  10. is stored in eye space. Also since we're talking about
  11. a directional light, the position field is actually direction */
  12. lightDir = normalize(vec3(gl_LightSource[ 0].position));
  13. /* Normalize the halfVector to pass it to the fragment shader */
  14. halfVector = normalize(gl_LightSource[ 0].halfVector.xyz);
  15. /* Compute the diffuse, ambient and globalAmbient terms */
  16. diffuse = gl_FrontMaterial.diffuse * gl_LightSource[ 0].diffuse;
  17. ambient = gl_FrontMaterial.ambient * gl_LightSource[ 0].ambient;
  18. ambient += gl_FrontMaterial.ambient * gl_LightModel.ambient;
  19. gl_Position = ftransform();
  20. }
接下來在片斷shader中,首先要宣告同樣的易變變數。此外還要再次對法線進行歸一化,光線向量不需要進行歸一化了,因為方向光對所有頂點都是一致的,插值得到的結果自然也不會變。之後就是計算插值過的法線向量與光線向量的點積。
  1. varying vec4 diffuse,ambient;
  2. varying vec3 normal,lightDir,halfVector;
  3. void main()
  4. {
  5. vec3 n,halfV;
  6. float NdotL,NdotHV;
  7. /* The ambient term will always be present */
  8. vec4 color = ambient;
  9. /* a fragment shader can't write a varying variable, hence we need
  10. a new variable to store the normalized interpolated normal */
  11. n = normalize(normal);
  12. /* compute the dot product between normal and ldir */
  13. NdotL = max(dot(n,lightDir), 0.0);
  14. ...
如果點積結果NdotL大於0,我們就必須計算散射光,也就是用頂點shader傳過來的散射項乘以這個點積。我們還需要計算鏡面反射光,計算時首先對接收到的半向量歸一化,然後計算半向量和法線之間的點積。
  1. ...
  2. if (NdotL > 0.0)
  3. {
  4. color += diffuse * NdotL;
  5. halfV = normalize(halfVector);
  6. NdotHV = max(dot(n,halfV), 0.0);
  7. color += gl_FrontMaterial.specular *
  8. gl_LightSource[ 0].specular *
  9. pow(NdotHV, gl_FrontMaterial.shininess);
  10. }
  11. gl_FragColor = color;
  12. }
下圖顯示了逐畫素光照和逐頂點光照效果的區別:

本節內容Shader Designer的工程下載地址:
http://www.lighthouse3d.com/wp-content/uploads/2011/03/dirpixsd.zip

逐畫素的點光(Point Light Per Pixel)
本節基於前面有關方向光的內容,大部分程式碼都相同。本節內容主要涉及方向光和點光的不同之處。方向光一般假設光源在無限遠的地方,所以到達物體時是平行光。相反,點光源有一個空間中的位置,並向四面八方輻射光線。此外,點光的強度會隨到達頂點的距離而衰弱。
對於OpenGL程式來說,這兩種光的區別主要有:
•光源的position域的w分量:對方向光來說它是0,表面這個position實際是一個方向(direction);對點光來說,這個分量是1。
•點光源的衰減由三個係數決定:一個常數項,一個線性項和一個二次項。
對方向光來說,光線的方向對所有頂點相同,但是對點光來說,方向是從頂點指向光源位置的向量。因此對我們來說需要修改的就是在頂點shader中加入計算光線方向的內容。
在OpenGL中衰減是按照如下公式計算的:

式中k0是常數衰減係數,k1是線性衰減係數,k2是二次衰減係數,d是光源位置到頂點的距離。
注意衰減與距離是非線性關係,所以我們不能逐頂點計算衰減再在片斷shader中使用插值結果,不過我們可以在頂點shader中計算距離,然後在片斷shader中使用距離的插值計算衰減。
使用點光計算顏色值的公式為:

在上面公式中,環境光部分必須分解為兩項:使用光照模型的全域性環境光設定和光源中的環境光設定。頂點shader也必須分別計算這兩個環境光成分。新的頂點shader如下:
  1. varying vec4 diffuse,ambientGlobal,ambient;
  2. varying vec3 normal,lightDir,halfVector;
  3. varying float dist;
  4. void main()
  5. {
  6. vec4 ecPos;
  7. vec3 aux;
  8. normal = normalize(gl_NormalMatrix * gl_Normal);
  9. /* these are the new lines of code to compute the light's direction */
  10. ecPos = gl_ModelViewMatrix * gl_Vertex;
  11. aux = vec3(gl_LightSource[ 0].position-ecPos);
  12. lightDir = normalize(aux);
  13. dist = length(aux);
  14. halfVector = normalize(gl_LightSource[ 0].halfVector.xyz);
  15. /* Compute the diffuse, ambient and globalAmbient terms */
  16. diffuse = gl_FrontMaterial.diffuse * gl_LightSource[ 0].diffuse;
  17. /* The ambient terms have been separated since one of them */
  18. /* suffers attenuation */
  19. ambient = gl_FrontMaterial.ambient * gl_LightSource[ 0].ambient;
  20. ambientGlobal = gl_FrontMaterial.ambient * gl_LightModel.ambient;
  21. gl_Position = ftransform();
  22. }
在片斷shader中需要計算衰減,還需要將插值得到的光線方向向量歸一化,因為一般來說照到每個頂點的光線方向都不同。
  1. varying vec4 diffuse,ambientGlobal, ambient;
  2. varying vec3 normal,lightDir,halfVector;
  3. varying float dist;
  4. void main()
  5. {
  6. vec3 n,halfV,viewV,ldir;
  7. float NdotL,NdotHV;
  8. vec4 color = ambientGlobal;
  9. float att;
  10. /* a fragment shader can't write a varying variable, hence we need
  11. a new variable to store the normalized interpolated normal */
  12. n = normalize(normal);
  13. /* compute the dot product between normal and normalized lightdir */
  14. NdotL = max(dot(n,normalize(lightDir)), 0.0);
  15. if (NdotL > 0.0)
  16. {
  17. att = 1.0 / (gl_LightSource[ 0].constantAttenuation +
  18. gl_LightSource[ 0].linearAttenuation * dist +
  19. gl_LightSource[ 0].quadraticAttenuation * dist * dist);
  20. color += att * (diffuse * NdotL + ambient);
  21. halfV = normalize(halfVector);
  22. NdotHV = max(dot(n,halfV), 0.0);
  23. color += att * gl_FrontMaterial.specular * gl_LightSource[ 0].specular *
  24. pow(NdotHV,gl_FrontMaterial.shininess);
  25. }
  26. gl_FragColor = color;
  27. }
下圖顯示了固定功能的逐頂點與本節中逐畫素計算得到的點光效果:

本節內容Shader Designer工程下載地址:
http://www.lighthouse3d.com/wp-content/uploads/2011/03/pointlightsd.zip

逐畫素的聚光(Spot Light Per Pixel)
本節內容與上一節基本一致,唯一不同的就是聚光不同於點光,其發出的光線被限制在一個圓錐體中。
對於OpenGL程式來說,這兩種光的區別主要有:
•聚光包含一個方向向量spotDirection,表示圓錐體的軸。
•圓錐體包含一個角度,在GLSL中可以使用應用程式設定的角度值以及對應的餘弦值spotCosCutoff。
•最後還有一個衰減速率spotExponent,它表示從圓錐的中心軸向外表面變化時光強度的衰減。
聚光的頂點shader與點光完全相同,我們只需要對片斷shader進行一些修改。只有噹噹前片斷位於聚光的光錐內時,才需要對散射光、鏡面反射光和環境光成分進行著色。所以我們首先要檢查這個條件。
光源與某點連線向量以及聚光方向向量(spotDirection)之間夾角的餘弦值必須大於spotCosCutoff,否則此點位於聚光之外,只能接收到全域性環境光。
  1. ...
  2. n = normalize(normal);
  3. /* compute the dot product between normal and ldir */
  4. NdotL = max(dot(n,normalize(lightDir)), 0.0);
  5. if (NdotL > 0.0)
  6. {
  7. spotEffect = dot(normalize(gl_LightSource[ 0].spotDirection),
  8. normalize(-lightDir));
  9. if (spotEffect > gl_LightSource[ 0].spotCosCutoff)
  10. {
  11. /* compute the illumination in here */
  12. }
  13. }
  14. gl_FragColor = ...
下面的光照計算與點光非常相似,唯一區別是衰減必須乘以聚光效果(spotlight effect),這個值按如下公式計算:

上式中spotDirection來自OpenGL中設定的狀態,lightDir是光源到某點的向量,spotExp是聚光衰減率,這個值也是在OpenGL程式中設定的,它用來控制從聚光光錐中心到邊緣的衰減。spotExp越大衰減越快,如果為0表示在光錐內光強是常數。
  1. spotEffect = pow(spotEffect, gl_LightSource[ 0].spotExponent);
  2. att = spotEffect / (gl_LightSource[ 0].constantAttenuation +
  3. gl_LightSource[ 0].linearAttenuation * dist +
  4. gl_LightSource[ 0].quadraticAttenuation * dist * dist);
  5. color += att * (diffuse * NdotL + ambient);
  6. halfV = normalize(halfVector);
  7. NdotHV = max(dot(n,halfV), 0.0);
  8. color += att * gl_FrontMaterial.specular *
  9. gl_LightSource[ 0].specular *
  10. pow(NdotHV,gl_FrontMaterial.shininess);
下圖分別顯示了使用固定功能流水線的逐頂點光照計算,以及使用本節shader的逐畫素光照計算得到的聚光效果。

本節內容Shader Designer的工程下載地址:
http://www.lighthouse3d.com/wp-content/uploads/2011/03/spotlightsd.zip