跳至内容

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部分
华夏公益教科书