LearnOpenGL note - Lighting:Light casters

OpenGL光照系列5

Posted by Tao on Saturday, March 5, 2022

0 Preface

本节笔记对应的内容为投光物,部分内容参考自傅老師/OpenGL教學 第二章

前面的课程我们实现的光照是一种类似于点光源的照明,但是又不存在衰减半径。它能给我们不错的效果,但现实世界中,我们有很多种类的光照,每种的表现都不同。将光投射(Cast)到物体的光源叫做投光物(Light Caster)。在这一节中,我们将会讨论几种不同类型的投光物。学会模拟不同种类的光源是又一个能够进一步丰富场景的工具。
我们首先将会讨论定向光(Directional Light),接下来是点光源(Point Light),它是我们之前学习的光源的拓展,最后我们将会讨论聚光(Spotlight)。

1 Directional Light

当一个光源处于很远的地方时,来自光源的每条光线就会近似于互相平行。当我们使用一个假设光源放置于无限远处时,它就被称为定向光,因为它的所有光线都有着相同的方向,它与光源的位置是没有关系的。
定向光非常好的一个例子就是太阳。太阳距离我们并不是无限远,但它已经远到在光照计算中可以把它视为无限远了。所以来自太阳的所有光线将被模拟为平行光线,我们可以在下图看到:
light_casters_directional
因为所有的光线都是平行的,所以物体与光源的相对位置是不重要的,因为对场景中每一个物体光的方向都是一致的。由于光的位置向量保持一致,场景中每个物体的光照计算将会是一致的。
我们可以定义一个光线方向向量而不是位置向量来模拟一个定向光。着色器的计算基本保持不变,但这次我们将直接通过外部传入的光的lightDir向量而不是通过direction来计算lightDir向量。

uniform vec3 lightDir;

void main() {
  // vec3 lightDir = normalize(lightPos - FragPos);
  vec3 reflectVec = reflect(-lightDir, Normal);
  vec3 cameraVec = normalize(cameraPos - FragPos);
  ...
}

注意我们首先对light.direction向量取反。我们目前使用的光照计算需求一个从片段至光源的光线方向,但人们更习惯定义定向光为一个从光源出发的全局方向。所以我们需要对全局光照方向向量取反来改变它的方向,它现在是一个指向光源的方向向量了。而且,记得对向量进行标准化,假设输入向量为一个单位向量是很不明智的。
最终的lightDir向量将和以前一样用在漫反射和镜面光计算中。

我们现在需要封装一下平行光部分的代码,加入LightDirectional类:
LightDirectional.h:

#ifndef _LIGHTDIRECTIONAL_H_
#define _LIGHTDIRECTIONAL_H_

#include <glm/glm.hpp>
#include <glm/gtx/rotate_vector.hpp>

class LightDirectional
{
public:
    LightDirectional(glm::vec3 _position, glm::vec3 _angles, glm::vec3 _color = glm::vec3(1.0f, 1.0f, 1.0f));
    ~LightDirectional();

    glm::vec3 position;
    glm::vec3 angles;
    glm::vec3 direction = glm::vec3(0, 0, 1.0f);
    glm::vec3 color;

    void UpdateDirection();
};

#endif

通过传入针对各坐标轴的旋转角计算目标向量

LightDirectional.cpp:

#include "LightDirectional.h"

LightDirectional::LightDirectional(glm::vec3 _position, glm::vec3 _angles, glm::vec3 _color)
    : position(_position), angles(_angles), color(_color)
{
    UpdateDirection();
}

LightDirectional::~LightDirectional()
{
}

void LightDirectional::UpdateDirection()
{
    direction = glm::vec3(0, 0, 1.0f); // pointing to z(forward)
    direction = glm::rotateZ(direction, angles.z);
    direction = glm::rotateX(direction, angles.x);
    direction = glm::rotateY(direction, angles.y);
    direction = -1.0f * direction;
}

传入光源位置、颜色及光源方向:

#include "LightDirectional.h"
......
#pragma region Light Declare
LightDirectional light (glm::vec3(10.0f, 10.0f, -5.0f),glm::vec3(glm::radians(45.0f),0,0));
#pragma endregion
......
glUniform3f(glGetUniformLocation(myShader->ID, "lightPos"), light.position.x,light.position.y,light.position.z);
glUniform3f(glGetUniformLocation(myShader->ID, "lightColor"), light.color.x,light.color.y,light.color.z);
glUniform3f(glGetUniformLocation(myShader->ID, "lightDir"), light.direction.x, light.direction.y, light.direction.z);

观察结果可以发现光源都来自一个方向

result

平行光完整代码(TODO)

2 Point lights

定向光对于照亮整个场景的全局光源是非常棒的,但除了定向光之外我们也需要一些分散在场景中的点光源(Point Light)。点光源是处于世界中某一个位置的光源,它会朝着所有方向发光,但光线会随着距离逐渐衰减。想象作为投光物的灯泡和火把,它们都是点光源。 light_casters_point 在之前的教程中,我们一直都在使用一个(简化的)点光源。我们在给定位置有一个光源,它会从它的光源位置开始朝着所有方向散射光线。然而,我们定义的光源模拟的是永远不会衰减的光线,这看起来像是光源亮度非常的强。在大部分的3D模拟中,我们都希望模拟的光源仅照亮光源附近的区域而不是整个场景。

Attenuation(衰减)

随着光线传播距离的增长逐渐削减光的强度通常叫做衰减(Attenuation)。随距离减少光强度的一种方式是使用一个线性方程。这样的方程能够随着距离的增长线性地减少光的强度,从而让远处的物体更暗。然而,这样的线性方程通常会看起来比较假。在现实世界中,灯在近处通常会非常亮,但随着距离的增加光源的亮度一开始会下降非常快,但在远处时剩余的光强度就会下降的非常缓慢了。所以,我们需要一个不同的公式来减少光的强度。
下面这个公式根据片段距光源的距离计算了衰减值,之后我们会将它乘以光的强度向量:
\[F_{att} = \frac{1.0}{K_{c} + K_{l} \ast d + K_{q} \ast d^{2}}\]

在这里\(d\)代表了片元距光源的距离。接下来为了计算衰减值,我们定义3个(可配置的)项:常数constant项\(K_{c}\)、一次linear项\(K_{l}\)和二次quadratic项\(K_{q}\)。

  • 常数项通常保持为1.0,它的主要作用是保证分母永远不会比1小,否则的话在某些距离上它反而会增加强度,这肯定不是我们想要的效果。
  • 一次项会与距离值相乘,以线性的方式减少强度。
  • 二次项会与距离的平方相乘,让光源以二次递减的方式减少强度。二次项在距离比较小的时候影响会比一次项小很多,但当距离值比较大的时候它就会比一次项更大了。

由于二次项的存在,光线会在大部分时候以线性的方式衰退,直到距离变得足够大,让二次项超过一次项,光的强度会以更快的速度下降。这样的结果就是,光在近距离时亮度很高,但随着距离变远亮度迅速降低,最后会以更慢的速度减少亮度。下面这张图显示了在100的距离内衰减的效果: attenuation 你可以看到光在近距离的时候有着最高的强度,但随着距离增长,它的强度明显减弱,并缓慢地在距离大约100的时候强度接近0。这正是我们想要的。

Choosing the right values

但是,该对这三个项设置什么值呢?正确地设定它们的值取决于很多因素:环境、希望光覆盖的距离、光的类型等。这些值一般都是经验值。下表显示了这些项可能取的一些值。第一列指定的是当前参数下光所能覆盖的距离。该数据由Ogre3D的Wiki所提供:

距离 常数项 一次项 二次项
7 1.0 0.7 1.8
13 1.0 0.35 0.44
20 1.0 0.22 0.20
32 1.0 0.14 0.07
50 1.0 0.09 0.032
65 1.0 0.07 0.017
100 1.0 0.045 0.0075
160 1.0 0.027 0.0028
200 1.0 0.022 0.0019
325 1.0 0.014 0.0007
600 1.0 0.007 0.0002
3250 1.0 0.0014 0.000007

你可以看到,常数项\(K_{c}\)在所有的情况下都是1.0。一次项\(K_{l}\)为了覆盖更远的距离通常都很小,二次项\(K_{q}\)甚至更小。尝试对这些值进行实验,看看它们在你的实现中有什么效果。在我们的环境中,32到100的距离对大多数的光源都足够了。

Implementing attenuation

为了实现衰减,在片段着色器中我们还需要三个额外的值:也就是公式中的常数项、一次项和二次项。现在新增LightPoint类:
LightPoint.h

#ifndef _LIGHTPOINT_H_
#define _LIGHTPOINT_H_

#include <glm/glm.hpp>
#include <glm/gtx/rotate_vector.hpp>

class LightPoint
{
public:
    LightPoint(glm::vec3 _position, glm::vec3 _angles, glm::vec3 _color = glm::vec3(1.0f, 1.0f, 1.0f));
    ~LightPoint();

    glm::vec3 position;
    glm::vec3 angles;
    glm::vec3 direction = glm::vec3(0, 0, 1.0f);
    glm::vec3 color;

    float constant;
    float linear;
    float quadratic;
};

#endif

我们希望光源能够覆盖50的距离,所以我们会使用表格中对应的常数项、一次项和二次项:
LightPoint.cpp

#include "LightPoint.h"

LightPoint::LightPoint(glm::vec3 _position, glm::vec3 _angles, glm::vec3 _color)
    : position(_position), angles(_angles), color(_color)
{
    constant = 1.0f;
    linear = 0.09f;
    quadratic = 0.032f;
}

片元着色器中新增LightPoint类,并且为了视觉效果,只对diffuse和specular做衰减,完整shader代码如下:

#version 330 core
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;

struct Material {
  vec3 ambient;
  sampler2D diffuse;
  sampler2D specular;
  float shininess;
};

struct LightPoint {
  float constant;
  float linear;
  float quadratic;
};

uniform Material material;
uniform LightPoint lightP;

uniform vec3 objColor;
uniform vec3 ambientColor;
uniform vec3 lightPos;
uniform vec3 lightDirUniform;
uniform vec3 lightColor;
uniform vec3 cameraPos;

out vec4 FragColor;
void main() {
  float dist = length(lightPos - FragPos);
  float attenuation = 1.0 / (lightP.constant + lightP.linear * dist +
                             lightP.quadratic * (dist * dist));

  vec3 lightDir = normalize(lightPos - FragPos);
  vec3 reflectVec = reflect(-lightDir, Normal);
  vec3 cameraVec = normalize(cameraPos - FragPos);

  // specular
  float specularAmount =
      pow(max(dot(reflectVec, cameraVec), 0), material.shininess);
  // vec3 specular = material.specular * specularAmount * lightColor;
  vec3 specular =
      texture(material.specular, TexCoord).rgb * specularAmount * lightColor;
  // diffuse
  // vec3 diffuse = material.diffuse * max(dot(lightDir, Normal), 0) *
  // lightColor;
  vec3 diffuse = texture(material.diffuse, TexCoord).rgb *
                 max(dot(lightDir, Normal), 0) * lightColor;
  // ambient
  vec3 ambient = texture(material.diffuse, TexCoord).rgb * ambientColor;
  FragColor =
      vec4((ambient + (diffuse + specular) * attenuation) * objColor, 1.0f);
}

主函数中新增传入上述三参数

glUniform3f(glGetUniformLocation(testShader->ID, "lightPos"), light.position.x, light.position.y, light.position.z);
glUniform3f(glGetUniformLocation(testShader->ID, "lightDirUniform"), light.direction.x, light.direction.y, light.direction.z);
glUniform3f(glGetUniformLocation(testShader->ID, "lightColor"), light.color.r, light.color.g, light.color.b);
glUniform1f(glGetUniformLocation(testShader->ID, "lightP.constant"), light.constant);
glUniform1f(glGetUniformLocation(testShader->ID, "lightP.linear"), light.linear);
glUniform1f(glGetUniformLocation(testShader->ID, "lightP.quadratic"), light.quadratic);

实现效果如下,可见光强随距离有明显衰减,远处的箱子颜色较暗:

result2

3 Spotlight

我们要讨论的最后一种类型的光是聚光(Spotlight)。聚光是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。这样的结果就是只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗。聚光很好的例子就是路灯或手电筒。

OpenGL中聚光是用一个世界空间位置、一个方向和一个切光角(Cutoff Angle)来表示的,切光角指定了聚光圆锥的半径。对于每个片元,我们会计算片元是否位于聚光的切光方向之间(也就是在锥形内),如果是的话,我们就会相应地照亮片段。下面这张图会让你明白聚光是如何工作的:

light_casters_spotlight_angles

  • LightDir:从片段指向光源的向量。
  • SpotDir:聚光所指向的方向。
  • Phi\(\phi\):指定了聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。
  • Theta\(\theta\):LightDir向量和SpotDir向量之间的夹角。在聚光内部的话\(\theta\)值应该比\(\phi\)值小。

所以我们要做的就是计算LightDir向量和SpotDir向量之间的点积(返回两个单位向量夹角的余弦值),并将它与切光角\(\phi\)值对比。

新建LightSpot类,LightSpot.h:

#ifndef _LIGHTSPOT_H_
#define _LIGHTSPOT_H_

#include <glm/glm.hpp>
#include <glm/gtx/rotate_vector.hpp>

class LightSpot
{
public:
    LightSpot(glm::vec3 _position, glm::vec3 _angles, glm::vec3 _color = glm::vec3(1.0f, 1.0f, 1.0f));
    ~LightSpot();

    glm::vec3 position;
    glm::vec3 angles;
    glm::vec3 direction = glm::vec3(0, 0, 1.0f);
    glm::vec3 color;

    float cosPhy = 0.9f;

    void UpdateDirection();
};

#endif

UpdateDiection函数的实现与DirectionalLight中相同。

fragment Shader:

#version 330 core
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;

struct Material {
  vec3 ambient;
  sampler2D diffuse;
  sampler2D specular;
  float shininess;
};

struct LightPoint {
  float constant;
  float linear;
  float quadratic;
};

struct LightSpot {
  float cosPhy;
};

uniform Material material;
uniform LightPoint lightP;
uniform LightSpot lightS;

uniform vec3 objColor;
uniform vec3 ambientColor;
uniform vec3 lightPos;
uniform vec3 lightDirUniform;
uniform vec3 lightColor;
uniform vec3 cameraPos;

out vec4 FragColor;
void main() {
  vec3 lightDir = normalize(lightPos - FragPos);
  vec3 reflectVec = reflect(-lightDir, Normal);
  vec3 cameraVec = normalize(cameraPos - FragPos);

  // specular
  float specularAmount =
      pow(max(dot(reflectVec, cameraVec), 0), material.shininess);
  // vec3 specular = material.specular * specularAmount * lightColor;
  vec3 specular =
      texture(material.specular, TexCoord).rgb * specularAmount * lightColor;
  // diffuse
  // vec3 diffuse = material.diffuse * max(dot(lightDir, Normal), 0) *
  // lightColor;
  vec3 diffuse = texture(material.diffuse, TexCoord).rgb *
                 max(dot(lightDir, Normal), 0) * lightColor;
  // ambient
  vec3 ambient = texture(material.diffuse, TexCoord).rgb * ambientColor;

  float cosTheta = dot(normalize(FragPos - lightPos), -1 * lightDirUniform);
  if (cosTheta > lightS.cosPhy) {
    // inside
    FragColor = vec4((ambient + (diffuse + specular)) * objColor, 1.0f);
  } else {
    // outside
    FragColor = vec4((ambient)*objColor, 1.0f);
  }
}

main文件中传入cosPhy的值

glUniform1f(glGetUniformLocation(testShader->ID, "lightS.cosPhy"), light.cosPhy);

result3

上图效果仍看起来有些假,主要是因为聚光有一圈硬边。当一个片段遇到聚光圆锥的边缘时,它会完全变暗,没有一点平滑的过渡。一个真实的聚光将会在边缘处逐渐减少亮度。

Smooth/Soft edges(平滑过渡)

为了创建一种看起来边缘平滑的聚光,我们需要模拟聚光有一个内圆锥(Inner Cone)和一个外圆锥(Outer Cone)。我们可以将内圆锥设置为上一部分中的那个圆锥,但我们也需要一个外圆锥,来让光从内圆锥逐渐减暗,直到外圆锥的边界。

为了创建一个外圆锥,我们只需要再定义一个余弦值来代表聚光方向向量和外圆锥向量(等于它的半径)的夹角。然后,如果一个片段处于内外圆锥之间,将会给它计算出一个0.0到1.0之间的强度值。如果片段在内圆锥之内,它的强度就是1.0,如果在外圆锥之外强度值就是0.0。

我们可以用下面这个公式来计算这个值:

\[I = \frac{\theta −\gamma }{\epsilon }\]

这里\(\epsilon \)(Epsilon)是内(\(\phi\))和外圆锥(\(\gamma\))之间的余弦值差(\(\epsilon = \phi −\gamma \))。最终的\(I\)值就是在当前片段聚光的强度。

lerp

LightSpot.h文件中新增cosPhyOutter

    float cosPhyInner = 0.9f;
    float cosPhyOutter = 0.85f;

fragmentSource中:

float spotRatio;
if (cosTheta > lightS.cosPhyInner) {
  // inside
  spotRatio = 1.0f;
} else if (cosTheta > lightS.cosPhyOutter) {
  // middle
  spotRatio = 1 - (cosTheta - lightS.cosPhyInner) /
                      (lightS.cosPhyOutter - lightS.cosPhyInner);
} else {
  // outside
  spotRatio = 0;
}
FragColor = vec4((ambient + diffuse + specular) * spotRatio * objColor, 1.0f);

result4

这样看起来光线就好多了。

「如果这篇文章对你有用,请随意打赏」

Heisenberg Blog

如果这篇文章对你有用,请随意打赏

使用微信扫描二维码完成支付


comments powered by Disqus