计算机图形学(OPENGL):光源

本文同时发布在我的个人博客上:https://dragon_boy.gitee.io

光源

  在现实生活中,有很多不同种类的光,它们有不同的表现形式。这里我们讨论点光源、平行光和聚光灯。

平行光

  当光源从很远的位置发出光时,这些光线可以近似看作相互平行,都有同一个传递方向,最典型的例子就是太阳光:



  由于光线相互平行,所以光源的位置并不重要,我们只需关心它的方向和光的参数。我们可以在片元着色器中这样定义一个平行光光源:

struce Light
{
  vec3 direction;
  vec3 ambient;
  vec3 diffuse;
  vec3 specular;
};
...
void main()
{
  vec3 lightDir = normalize(-light.direction);
}

  我们定义的平行光的方向符合常识,指向要照亮的物体,但为了方便计算,我们需要光的方向从物体指向光源。那么接下来的工作就和之前一致,分别计算对环境光、漫反射光、高光的值。为了方便观察,我们将绘制10个不同状态的立方体来观察平行光的影响。
  首先定义十个立方体的位置:

glm::vec3 cubePositions[] = {
    glm::vec3( 0.0f,  0.0f,  0.0f),
    glm::vec3( 2.0f,  5.0f, -15.0f),
    glm::vec3(-1.5f, -2.2f, -2.5f),
    glm::vec3(-3.8f, -2.0f, -12.3f),
    glm::vec3( 2.4f, -0.4f, -3.5f),
    glm::vec3(-1.7f,  3.0f, -7.5f),
    glm::vec3( 1.3f, -2.0f, -2.5f),
    glm::vec3( 1.5f,  2.0f, -2.5f),
    glm::vec3( 1.5f,  0.2f, -1.5f),
    glm::vec3(-1.3f,  1.0f, -1.5f)
};

  在一个循环中让每个立方体都转一下:

for(unsigned int i = 0; i < 10; i++)
{
    glm::mat4 model = glm::mat4(1.0f);
    model = glm::translate(model, cubePositions[i]);
    float angle = 20.0f * i;
    model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
    lightingShader.setMat4("model", model);

    glDrawArrays(GL_TRIANGLES, 0, 36);
}

  当然,还要设置平行光的方向:

lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f); //光源指向下方

  注意到这里并没有定义平行光方向向量的w组件,其实也可以定义,但这里我们关心的是平行光的方向,我们不想让它受平移变换的影响,所以可以设w组件为0.0;如果我们更加关心光源的位置,我们就可以将w组件设为1.0:

if(lightVector.w == 0.0) // note: be careful for floating point errors
  // 进行平行光计算
else if(lightVector.w == 1.0)
  // 进行光源计算(利用光源的位置)

  运行程序,结果如下:


  这里贴出原文源码:Code。

点光源

  许多的点光源可以为场景营造更多的细节。一个点光源向四周散发出光,同时光会随距离衰减:



  在之前我们已经模拟过一个没有衰减的点光源,这里来为它加上衰减。

衰减

  仔细观察可以发现,光的衰减并不符合线性变化,往往在某段距离衰减很慢,之后又会很快,所以我们不能简单的用线性变化方式去模拟,这里给出一个比较好的模拟公式:


  这个公式计算的衰减系数会之后与光源的强度向量相乘。其中,d代表物体到光源的距离,K_c时常数项,K_l时线性项系数,K_q是二次项系数。

  • 常数项永远置为1来确保公式的分母不会大于1即衰减系数不会大于1。
  • 线性项让衰减呈线性变化。
  • 二次项让衰减在距离较短时影响小于线性项,在距离较远时影响高于线性项。
      公式的衰减大致呈如下分布:



      下面是为了模拟真实情况的在不同离散距离的不同系数取值:



      在这里我们选用距离为32到100的系数值。
      根据上面的知识,我们就可以这样定义一个点光源的结构体:
struct Light
{
  vec3 position;
  
  vec3 ambient;
  vec3 diffuse;
  vec3 specular;

  float constant;   //常量
  float linear;  //一次项
  float quadratic; //二次项
};

  这里我们让衰减的范围为50,并设置相关系数:

lightingShader.setFloat("light.constant",  1.0f);
lightingShader.setFloat("light.linear",    0.09f);
lightingShader.setFloat("light.quadratic", 0.032f); 

  接下来需要知道片段到光源的距离,这里使用内建的length函数,之后通过上面的衰减公式计算衰减系数:

float distance    = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));    

  同样,在计算环境光、漫反射、高光之后我们分别乘以衰减系数:

ambient  *= attenuation; 
diffuse  *= attenuation;
specular *= attenuation;  

  运行程序就可以得到这样的结果:


  可以看到衰减的效果很明显。
  这里给出原文代码:Code。

聚光灯

  聚光灯产生的是一片从光源出发的锥形区域。在区域内的物体将被照亮,可以想象为手电筒。在OpenGL中聚光灯的参数为世界空间的位置,方向,切割角度来表示。我们通过计算光源方向向量与光源到片段的构成的向量的夹角,并与切割角度进行比较来判断某一片段是否被照亮。下面是一个聚光灯的例子:
  • LightDir是一个由片段指向光源的向量。
  • SpotDir是聚光灯的方向。
  • \phi是切割角度。
  • \theta是LightDir和SpotDir的夹角。
      接下来的例子我们将在摄像机的位置创建一个聚光灯。
      我们根据聚光灯的特性创建结构体:
struct Light
{
  vec3 position;
  vec3 direction;
  
  vec3 ambient;
  vec3 diffuse;
  vec3 specular;

  float cutOff;
};

  接着我们赋值,由于计算角度比较麻烦,开销较大,我们将比较余弦值:

lightingShader.setVec3("light.position",  camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
lightingShader.setFloat("light.cutOff",   glm::cos(glm::radians(12.5f)));

  接着我们计算要比较的\theta的余弦值,并进行比较:

float theta = dot(lightDir, normalize(-light.direction));
    
if(theta > light.cutOff) //在0-90度范围内,余弦值越大,角度越小
{       
  // 进行灯光计算
}
else  // 为聚光灯范围之外的区域赋予环境光的颜色
  color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);

  运行结果如下:


  这里给出原文代码:Code。可以看到结果并不好,边缘非常锐利,下面我们来平滑一下边缘。
  我们可以通过一个内部锥体和一个外部锥体模型来模拟平滑边缘的聚光灯。我们将内部锥体保持不变,内部的光为1.0;让内部锥体边缘到外部锥体的光进行衰减,范围为0.0到1.0;外部锥体以外的光为0.0。我们通过下面的公式来模拟:

  这个公式中\epsilon是内锥体和外锥体的切割角的角度差,\gamma是外锥体的切割角,结果I就是内锥体和外锥体之间光的强度。下面是部分离散结果:

  注意上面的的结果我们均用角度的余弦值来近似。
  接下来我们修改一下聚光灯的结构体,加上外锥体切割角:

struct Light
{
  vec3 position;
  vec3 direction;
  
  vec3 ambient;
  vec3 diffuse;
  vec3 specular;

  float cutOff;
  float outerCutOff;
};

  接着计算\epsilon和I,并为漫反射和高光添加系数的影响:

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;
...

  注意我们对光强的计算使用了clamp函数来保证结果不会超出[0,1]的范围。
  我们将内切割角设为12.5度,外切割角设为17.5度,结果如下:


  可以看到效果很好了。这里给出原文代码:Code
  最后,给出原文链接:https://learnopengl.com/Lighting/Light-casters。

最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容