跳至內容

GLSL 程式設計/GLUT/漫反射

來自華夏公益教科書
從月球表面反射的光(在一個很好的近似中)只是漫反射。

本教程涵蓋每個頂點的漫反射

它是關於 OpenGL 2.x 中基本光照的一系列教程中的第一個。在本教程中,我們從單個方向光源的漫反射開始,然後包含點光源和聚光燈。進一步的教程涵蓋了這一點的擴充套件,特別是鏡面反射、逐畫素光照、雙面光照和多個光源。

在適當的時候,我們將使用與 OpenGL 1.x 相同的約定,以便那些升級 OpenGL 知識的人感到舒適。

漫反射可以使用表面法線向量 N 和光向量 L 計算,即指向光源的向量。

漫反射

[編輯 | 編輯原始碼]

月球幾乎只表現出漫反射(也稱為朗伯反射),即光向所有方向反射,沒有鏡面高光。其他此類材料的例子包括粉筆和啞光紙;事實上,任何看起來暗淡和啞光的表面。

在完美漫反射的情況下,觀察到的反射光強度取決於表面法線向量和入射光線的夾角的餘弦。如圖左側所示,通常考慮從表面某個點開始的歸一化向量,在那裡應該計算照明:歸一化的表面法線向量N垂直於表面,歸一化的光方向L指向光源。

對於觀察到的漫反射光,我們需要歸一化的表面法線向量N和歸一化的指向光源方向L的夾角的餘弦,這是點積N·L,因為任何兩個向量ab的點積a·b

.

對於歸一化向量,長度 |a| 和 |b| 均為 1。

如果點積N·L為負,則光源位於表面的“錯誤”一側,我們應該將反射設定為 0。這可以透過使用 max(0, N·L) 來實現,這確保點積的值對於負點積被鉗制為 0。此外,反射光取決於入射光強度和一個材料常數用於漫反射:對於黑色表面,材料常數為 0,對於白色表面,它為 1。漫反射強度的方程為

對於彩色光,此方程適用於每個顏色分量(例如紅色、綠色和藍色)。因此,如果變數,和表示顏色向量,乘法是逐分量執行的(它們在 GLSL 中是針對向量的),此方程也適用於彩色光。這正是我們在著色器程式碼中實際使用的。

一個方向光源的著色器程式碼

[編輯 | 編輯原始碼]

如果我們只有一個方向光源,用於實現方程的著色器程式碼相對較小。為了實現這個方程,我們遵循關於實現方程的問題,這些問題在輪廓增強教程中討論過。

  • 這個方程應該在頂點著色器還是片段著色器中實現?我們在這裡嘗試頂點著色器。在平滑鏡面高光教程中,我們將研究在片段著色器中的實現。
  • 應該在哪個座標系中實現這個方程?我們選擇了世界空間,這樣燈光就可以在與定位物體相同的座標系中描述。
  • 我們從哪裡獲得引數?這個問題的答案有點長。

檢視空間中的著色教程中所述,我們將漫射材質顏色作為material.diffuse提供。檢視空間中光源的方向Llight0.position中提供(第四個向量元素設定為0.0),而光線顏色light0.diffuse中提供。我們從屬性v_normal獲取物體座標中的表面法向量。由於我們在世界空間中實現這個方程,因此我們必須將表面法向量從物體空間轉換為世界空間,如輪廓增強教程中所述。

頂點著色器看起來像這樣

attribute vec4 v_coord;
attribute vec3 v_normal;
uniform mat4 m, v, p;
uniform mat3 m_3x3_inv_transp;
varying vec4 color;

struct lightSource
{
  vec4 position;
  vec4 diffuse;
};
lightSource light0 = lightSource(
    vec4(-1.0, 1.0, -1.0, 0.0),
    vec4(1.0, 1.0, 1.0, 1.0)
);

struct material
{
  vec4 diffuse;
};
material mymaterial = material(vec4(1.0, 0.8, 0.8, 1.0));

void main(void)
{
  mat4 mvp = p*v*m;
  vec3 normalDirection = normalize(m_3x3_inv_transp * v_normal);
  vec3 lightDirection = normalize(vec3(light0.position));

  vec3 diffuseReflection
    = vec3(light0.diffuse) * vec3(mymaterial.diffuse)
    * max(0.0, dot(normalDirection, lightDirection));

  color = vec4(diffuseReflection, 1.0);
  gl_Position = mvp * v_coord;
}

片段著色器是

varying vec4 color;

void main(void)
{
  gl_FragColor = color;
}

目前,我們只在場景中建立一個方向光源。

點光源的變化

[編輯 | 編輯原始碼]

在方向光源的情況下,light0.position指定光線來自的方向。然而,在點光源(或聚光燈)的情況下,light0.position將指定光源在檢視空間中的位置,我們必須計算光線方向作為從檢視空間中頂點位置到光源位置的差向量。由於點的第四個座標是1,而方向的第四個座標是0,因此我們可以輕鬆區分這兩種情況。

  vec3 lightDirection;

  if (light0.position.w == 0.0) // directional light
    {
      lightDirection = normalize(vec3(light0.position));
    }
  else // point or spot light
    {
      lightDirection = normalize(vec3(light0.position - m * v_coord));
    }

雖然方向光源沒有光線衰減,但我們應該在點光源和聚光燈源中新增一些距離衰減。當光線從三維空間中的一個點向外擴散時,它覆蓋的虛擬球體在更遠的距離處越來越大。由於這些球體的表面積隨著半徑的增加而二次增加,而每個球體的總光量是相同的,因此每單位面積的光量隨著與點光源距離的增加而二次減少。因此,我們應該將光源強度除以頂點的平方距離。

我們可以指定距離二次衰減(相當快)、距離線性衰減和恆定衰減的組合。程式碼如下:

struct lightSource
{
  vec4 position;
  vec4 diffuse;
  float constantAttenuation, linearAttenuation, quadraticAttenuation;
};
lightSource light0 = lightSource(
    vec4(-1.0, 1.0, -1.0, 1.0),
    vec4(1.0, 1.0, 1.0, 1.0),
    1.0, 0.0, 0.0
);
  vec3 lightDirection;
  float attenuation;

  if (light0.position.w == 0.0) // directional light
    {
      attenuation = 1.0; // no attenuation
      lightDirection = normalize(vec3(light0.position));
    }
  else // point or spot light
    { 
      vec3 vertexToLightSource = vec3(light0.position - m * v_coord);
      float distance = length(vertexToLightSource);
      attenuation = 1.0 / (light0.constantAttenuation 
                           + light0.linearAttenuation * distance
                           + light0.quadraticAttenuation * distance * distance);
      lightDirection = normalize(vertexToLightSource);
    }

然後應該將因子attenuation乘以light0.diffuse以計算入射光;請參見下面的完整著色器程式碼。請注意,聚光燈源具有額外的功能,將在下一節中討論。

還要注意,此程式碼不太可能為您提供最佳效能,因為任何if 通常都非常昂貴。由於light0.position.w是0或1,實際上重寫程式碼以避免使用if並進一步最佳化並不太難。

  vec3 vertexToLightSource = vec3(light0.position - m * v_coord * light0.position.w);
  float distance = length(vertexToLightSource);
  attenuation = mix(1.0,
                    1.0 / (light0.constantAttenuation 
                           + light0.linearAttenuation * distance
                           + light0.quadraticAttenuation * distance * distance),
                    gl_LightSource[0].position.w);
  lightDirection = vertexToLightSource / distance;

但是,出於清晰起見,我們將使用包含if的版本。(“保持簡單,笨蛋!”)

聚光燈的變化

[編輯 | 編輯原始碼]

僅當light0.spotCutoff小於或等於90.0時,第0個光源才是聚光燈(否則將其設定為180.0)。因此,測試可以是

      if (light0.spotCutoff <= 90.0) // spotlight

聚光燈的形狀由變數light0.spotDirectionlight0.spotExponentlight0.spotCutoff描述。具體來說,如果-lightDirectionlight0.spotDirection之間的角度的餘弦值小於light0.spotCutoff的餘弦值,即如果陰影點位於圍繞聚光燈方向的光錐之外,則衰減因子設定為。我們可以透過兩個歸一化向量的點積來計算兩個向量之間角度的餘弦值。(在 OpenGL 1.x 中,light0.spotCutoff 的餘弦值作為 gl_LightSource[0].spotCosCutoff 提供。)如果我們將餘弦值限制在 0 以忽略聚光燈後面的點,則測試為

          float clampedCosine = max(0.0, dot(-lightDirection, normalize(light0.spotDirection)));
          if (clampedCosine < cos(light0.spotCutoff * 3.14159 / 180.0)) // outside of spotlight cone
            {
              attenuation = 0.0;
            }

否則,如果一個點在光錐內,OpenGL 中的衰減應該用這種方式計算

              attenuation = attenuation * pow(clampedCosine, light0.spotExponent);

這允許更寬(較小的spotExponent)和更窄(較大的spotExponent)的聚光燈。

完整著色器程式碼

[編輯 | 編輯原始碼]

總而言之,我們針對單個方向光源、點光源或帶有線性衰減的聚光燈的新頂點著色器變為

attribute vec4 v_coord;
attribute vec3 v_normal;
uniform mat4 m, v, p;
uniform mat3 m_3x3_inv_transp;
varying vec4 color;

struct lightSource
{
  vec4 position;
  vec4 diffuse;
  float constantAttenuation, linearAttenuation, quadraticAttenuation;
  float spotCutoff, spotExponent;
  vec3 spotDirection;
};
lightSource light0 = lightSource(
    vec4(0.0,  1.0,  2.0, 1.0),
    vec4(1.0, 1.0, 1.0, 1.0),
    0.0, 1.0, 0.0,
    80.0, 20.0,
    vec3(-1.0, -0.5, -1.0)
);

struct material
{
  vec4 diffuse;
};
material mymaterial = material(vec4(1.0, 0.8, 0.8, 1.0));

void main(void)
{
  mat4 mvp = p*v*m;
  vec3 normalDirection = normalize(m_3x3_inv_transp * v_normal);
  vec3 lightDirection;
  float attenuation;

  if (light0.position.w == 0.0) // directional light
    {
      attenuation = 1.0; // no attenuation
      lightDirection = normalize(vec3(light0.position));
    }
  else // point or spot light (or other kind of light)
    {
      vec3 vertexToLightSource = vec3(light0.position - m * v_coord);
      float distance = length(vertexToLightSource);
      lightDirection = normalize(vertexToLightSource);
      attenuation = 1.0 / (light0.constantAttenuation
			   + light0.linearAttenuation * distance
			   + light0.quadraticAttenuation * distance * distance);

      if (light0.spotCutoff <= 90.0) // spotlight
	{
	  float clampedCosine = max(0.0, dot(-lightDirection, normalize(light0.spotDirection)));
	  if (clampedCosine < cos(light0.spotCutoff * 3.14159 / 180.0)) // outside of spotlight cone
	    {
	      attenuation = 0.0;
	    }
	  else
	    {
              attenuation = attenuation * pow(clampedCosine, light0.spotExponent);
	    }
	}
    }
  vec3 diffuseReflection = attenuation
    * vec3(light0.diffuse) * vec3(mymaterial.diffuse)
    * max(0.0, dot(normalDirection, lightDirection));

  color = vec4(diffuseReflection, 1.0);
  gl_Position = mvp * v_coord;
}

片段著色器仍然是

varying vec4 color;

void main(void) {
  gl_FragColor = color;
}

在您的 C++ 原始碼中,確保您更新m_3x3_inv_transp

  /* Transform normal vectors with transpose of inverse of upper left
     3x3 model matrix (ex-gl_NormalMatrix): */
  glm::mat3 m_3x3_inv_transp = glm::transpose(glm::inverse(glm::mat3(mesh.object2world)));
  glUniformMatrix3fv(uniform_m_3x3_inv_transp, 1, GL_FALSE, glm::value_ptr(m_3x3_inv_transp));

恭喜!您已經瞭解了關於 OpenGL 光源的很多知識。這對以下有關更高階光照的教程至關重要。具體來說,我們已經看到了

  • 什麼是漫射反射以及如何用數學方式描述它。
  • 如何在著色器中實現單個方向光源的漫射反射。
  • 如何將著色器擴充套件到帶有線性衰減的點光源。
  • 如何進一步擴充套件著色器以處理聚光燈。

進一步閱讀

[編輯 | 編輯原始碼]

如果您還想了解更多


< GLSL Programming/GLUT

除非另有說明,否則本頁上的所有示例原始碼均歸屬公共領域。
返回OpenGL程式設計 - 光照部分 返回GLSL程式設計 - GLUT部分
華夏公益教科書