OpenGL学习笔记15-Light casters

Light casters

Lighting/Light-casters

到目前为止,我们使用的所有照明都来自于一个单一的光源,即空间中的一个点。它的效果很好,但在现实世界中,我们有几种不同类型的光,每种光的作用都不同。将光投射到物体上的光源称为光源施法者。在本章中,我们将讨论几种不同类型的光源。学习模拟不同的光源是您工具箱中的另一个工具,可以进一步丰富您的环境。

我们将首先讨论一个方向光,然后是一个点光,这是我们之前所拥有的扩展,最后我们将讨论聚光灯。在下一章中,我们将把几种不同的光类型合并到一个场景中。

Directional Light

当光源距离较远时,从该光源发出的光线彼此接近平行。看起来所有的光线都来自同一个方向,不管物体和/或观察者在哪里。当一个光源被建模为无限远时,它被称为定向光,因为它所有的光线都有相同的方向;它与光源的位置无关。

定向光源的一个很好的例子就是我们所知道的太阳。太阳离我们不是无限远,但它是如此之远,我们可以感知到它是无限远的照明计算。所有来自太阳的光线都被模拟成平行光线,如下图所示:

因为所有的光线都是平行的,所以每个物体与光源位置的关系并不重要,因为场景中的每个物体的光线方向都是相同的。因为光线的方向向量保持不变,所以场景中的每个物体的光线计算都是相似的。

我们可以通过定义光的方向向量而不是位置向量来建模这样的定向光。着色器的计算基本保持不变,除了这次我们直接使用光的方向向量,而不是使用光的位置向量来计算lightDir向量:


struct Light {
    // vec3 position; // no longer necessary when using directional lights.
    vec3 direction;
  
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
[...]
void main()
{
  vec3 lightDir = normalize(-light.direction);
  [...]
}

注意,我们首先否定了光。方向向量。到目前为止,我们使用的光线计算期望光线方向是从碎片到光源的方向,但人们通常更喜欢指定一个方向光作为一个全局方向,从光源指向。因此我们必须否定全局光的方向矢量来改变它的方向;它现在是指向光源的方向矢量。另外,一定要对向量进行标准化,因为假设输入向量是单位向量是不明智的。

这样得到的lightDir向量就像之前一样用于漫反射和镜面计算。

为了清楚地证明一个方向光对多个对象有相同的效果,我们回顾了坐标系统一章末尾的容器聚会场景。如果你错过了聚会,我们定义了10个不同的容器位置container positions ,并为每个容器生成了不同的模型矩阵,其中每个模型矩阵包含适当的本地到世界的转换:


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

我们将光线的位置和方向向量定义为vec3s已经有一段时间了,但是有些人倾向于将所有的向量定义为vec4。当将位置向量定义为vec4时,将w组件设置为1.0是很重要的,这样平移和投影就能得到正确的应用。然而,当定义一个方向矢量为vec4时,我们不希望平移有任何影响(因为它们只表示方向,没有其他的),所以我们将w分量定义为0.0。

方向向量可以表示为:vec4(-0.2f, -1.0f, -0.3f, 0.0f)。这也可以作为一个简单的检查光线类型的功能:你可以检查w组件是否等于1。0,看看我们现在有一个光线的位置矢量,如果w等于0,我们有一个光线的方向矢量;因此,在此基础上调整计算:


if(lightVector.w == 0.0) // note: be careful for floating point errors
  // do directional light calculations
else if(lightVector.w == 1.0)
  // do light calculations using the light's position (as in previous chapters)  

有趣的事实:这实际上是旧的OpenGL(固定功能)如何决定一个光源是方向光源还是位置光源,并据此调整它的照明。

如果现在编译应用程序并快速浏览场景,就会发现有一个类似太阳的光源将光线投射到所有对象上。你能看到漫反射和镜面组件的反应,好像有一个光源在天空的某个地方?它是这样的:

 

您可以在这里找到应用程序的完整源代码here. 。

Point lights

定向光对于照亮整个场景的全局光来说非常好,但是我们通常也需要一些分散在整个场景中的点光。点光源是世界上某个特定位置的光源,它会向各个方向发光,光线会因距离太远而消失。可以把灯泡和手电筒看作是点光源。

在前几章中,我们使用了一个简单的点法。我们有一个光源,在一个给定的位置,散射光从那个给定的位置到各个方向。然而,我们定义的光源是模拟的光线,永不褪色,因此看起来光源非常强。在大多数3D应用程序中,我们希望模拟一个光源,它只照亮靠近光源的区域,而不是整个场景。

如果你从前面的章节中添加10个容器到照明场景中,你会注意到后面的容器被照亮的强度与前面的容器相同;在距离上减弱光的作用还没有逻辑可循。我们希望后面的容器与靠近光源的容器相比只被轻微地照亮。

Attenuation 衰减

在光线传播的距离上降低光的强度通常称为衰减。减少光强度的一种方法是简单地使用一个线性方程。这样的方程会线性地降低距离上的光强度,从而确保远处的物体亮度较低。然而,这样的线性函数看起来有点假。在现实世界中,光源在近处一般都很亮,但光源的亮度在远处迅速衰减;剩余的光强度随着距离的增加而逐渐减小。因此,我们需要一个不同的公式来降低光的强度。

幸运的是,一些聪明人已经为我们解决了这个问题。下面的公式计算了一个基于片段到光源的距离的衰减值,之后我们将其与光的强度向量相乘:

 

这里的表示从碎片到光源的距离。然后,为了计算衰减值,我们定义了3个(可配置的)项:常数项,线性项和二次项

  • 常数项通常保持在1.0,这主要是为了确保分母永远不会小于1,因为它会增加一定距离的强度,这不是我们想要的效果。
  • 线性项与以线性方式减少强度的距离值相乘。
  • 二次项与距离的象限相乘,并设置光源强度的二次衰减。当距离较小时,二次项与线性项相比不显著,但随着距离的增大而增大。

由于有二次项,光以线性方式衰减,直到距离足够大,二次项超过了线性项,光强衰减的速度就会快得多。其结果是,光线在近距离时相当强烈,但很快就会在距离上失去亮度,直到最终以更慢的速度失去亮度。下面的图表显示了这种衰减在100个距离上的影响:

 

你可以看到,当距离很小的时候,光的强度最高,但是当距离增大的时候,光的强度显著降低,在距离100左右的时候慢慢的达到0。这正是我们想要的。

Choosing the right values

但是这3项的值是多少呢?设置正确的值取决于许多因素:环境,你想要的光覆盖的距离,光的类型等等。在大多数情况下,这只是一个经验和适度的调整的问题。下表显示了这些术语可以用来模拟覆盖特定半径(距离)的真实(某种)光源的一些值。第一列指定光将覆盖的距离与给定的条件。这些值对于大多数灯光来说都是很好的起点,感谢Ogre3D的wiki:Ogre3D's wiki:

DistanceConstantLinearQuadratic
71.00.71.8
131.00.350.44
201.00.220.20
321.00.140.07
501.00.090.032
651.00.070.017
1001.00.0450.0075
1601.00.0270.0028
2001.00.0220.0019
3251.00.0140.0007
6001.00.0070.0002
32501.00.00140.000007

可以看到,常数项KcKc在所有情况下都保持在1.0。线性项KlKl通常很小,可以覆盖更大的距离,而二次项KqKq甚至更小。尝试对这些值进行一些试验,看看它们在您的实现中的效果。在我们的环境中,32到100的距离对于大多数光线来说已经足够了。

Implementing attenuation

为了实现衰减,我们将需要3个额外的值在碎片着色器:即常数,线性和二次项的方程。这些最好存储在我们之前定义的Light struct中。注意,我们需要再次使用位置来计算lightDir,因为这是一个点光源(正如我们在前一章所做的),而不是方向光源。


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

在片段着色器中实现衰减是相对简单的:我们简单地计算一个基于等式的衰减值,并将其与环境、漫反射和镜面组件相乘。

我们确实需要到光源的距离来让方程生效。还记得我们如何计算一个向量的长度吗?我们可以通过计算碎片和光源之间的差向量来检索距离项,并取得到的向量的长度。我们可以使用GLSL的内置长度函数来实现这个目的:


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;   

如果你运行这个应用程序,你会得到这样的东西:

 

你可以看到现在只有前面的容器被照亮,最近的容器是最亮的。后面的容器根本没有被点亮,因为它们离光源太远了。您可以在这里找到应用程序的源代码。here.

点光源因此是一个具有可配置位置和应用于其照明计算的衰减的光源。这是我们照明库的另一种类型的光。

Spotlight 聚焦

我们要讨论的最后一种光是聚光灯。聚光灯是一个位于环境某处的光源,它不是向四面八方发射光线,而是只向一个特定的方向发射。结果是只有在聚光灯方向的一定半径范围内的物体被照亮,而其他一切都保持黑暗。聚光灯的一个很好的例子是路灯或手电筒。

OpenGL中的聚光灯由一个世界空间位置、一个方向和一个指定聚光灯半径的截止角来表示。对于每个片段,我们计算片段是否在聚光灯的截止方向之间(因此在它的锥形中),如果是这样,我们点燃相应的片段。下面的图片告诉你聚光灯是如何工作的:

 

  • LightDir: the vector pointing from the fragment to the light source.从碎片指向光源的矢量。
  • SpotDir: the direction the spotlight is aiming at.聚光灯瞄准的方向。
  • Phi ϕϕ : 指定聚光灯半径的截止角。这个角度以外的一切都不被聚光灯照亮。
  • Theta θθ :LightDir向量和SpotDir向量之间的角度。θθ值应该小于ΦΦ在聚光灯下。

 

所以我们需要做的是,计算点积(返回两个单位向量夹角的余弦值)之间的LightDir向量和SpotDir向量和比较这截止角ϕϕ。现在你(有点)了解了什么是聚光灯,我们要创建一个形式的手电筒。

Flashlight 闪光灯

手电筒是一个位于观众位置的聚光灯,通常从玩家的角度直接瞄准前方。手电筒基本上是一种普通的聚光灯,但它的位置和方向会根据玩家的位置和方向不断更新。

所以,我们需要的值片段着色器是聚光灯的位置向量(计算碎片到光线的方向向量),聚光灯的方向向量和截止角。我们可以在Light struct中存储这些值:


struct Light {
    vec3  position;
    vec3  direction;
    float cutOff;
    ...
};    

接下来,我们将适当的值传递给着色器:


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

正如你所看到的,我们没有为截止值设置一个角度,而是计算基于角度的余弦值,并将余弦结果传递给片段着色器。这样做的原因是在片段着色器中我们在计算LightDir和SpotDir向量之间的点积而点积返回的是一个余弦值而不是一个角度;我们不能直接比较角度和余弦值。为了在着色器中得到角度我们必须计算点积结果的反余弦这是一个昂贵的操作。因此,为了节省一些性能,我们事先计算给定截止角的余弦值,并将此结果传递给片段着色器。既然两个角现在都用余弦表示,我们就可以直接比较它们而不需要昂贵的操作。

现在剩下要做的是计算出θθθ值和比较这截止ϕϕ值来确定我们在或在聚光灯下:


float theta = dot(lightDir, normalize(-light.direction));
    
if(theta > light.cutOff) 
{       
  // do lighting calculations
}
else  // else, use ambient light so scene isn't completely dark outside the spotlight.
  color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);

我们首先计算lightDir向量和否定的方向向量(否定的,因为我们想让向量指向光源,而不是来自光源)之间的点积。一定要对所有相关向量进行标准化。

您可能想知道,为什么在if保护中使用>符号而不是<符号。在聚光灯内,theta不应该小于光的截止值吗?没错,但别忘了角度值是用余弦值表示的,角度0度表示为余弦值为1。0度表示为余弦值为1。90度表示为余弦值为0。

 

你现在可以看到余弦值越接近1。0,它的角度就越小。现在就明白了为什么需要大于截止值。截止值目前设置为余弦12.5,等于0.976,所以余弦值在0.976和1.0之间会导致片段被点燃,就像在聚光灯里面一样。

运行该应用程序会产生一个聚光灯,它只照亮直接在聚光灯锥内的片段。它是这样的:

 

您可以在这里找到完整的源代码here. 。

不过它看起来还是有点假,主要是因为聚光灯有硬边。当一个碎片到达聚光灯锥的边缘时,它就会被完全关闭,而不是用一个漂亮的平滑褪色。一个真实的聚光灯会逐渐减少它周围的光线。

Smooth/Soft edges 光滑/软边缘

为了创建一个平滑边缘聚光灯的效果,我们想要模拟一个有一个内锥和一个外锥的聚光灯。我们可以将内锥设置为上一节中定义的锥,但是我们还需要一个外锥,它可以使从内锥到外锥的边缘的光线逐渐变暗。

要创建一个外锥,我们只需定义另一个余弦值,它表示聚光灯的方向向量和外锥的向量之间的夹角(等于它的半径)。然后,如果一个片段位于内锥和外锥之间,它应该计算一个介于0.0和1.0之间的强度值。如果碎片在内锥内,则其强度等于1.0;如果碎片在外锥外,则强度等于0.0。

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

这个公式实际上是如何工作的有点难以想象,所以让我们用几个样本值来试试:

θθθθ in degreesϕϕ (inner cutoff)ϕϕ in degreesγγ (outer cutoff)γγ in degreesϵϵII
0.87300.91250.82350.91 - 0.82 = 0.090.87 - 0.82 / 0.09 = 0.56
0.9260.91250.82350.91 - 0.82 = 0.090.9 - 0.82 / 0.09 = 0.89
0.97140.91250.82350.91 - 0.82 = 0.090.97 - 0.82 / 0.09 = 1.67
0.83340.91250.82350.91 - 0.82 = 0.090.83 - 0.82 / 0.09 = 0.11
0.64500.91250.82350.91 - 0.82 = 0.090.64 - 0.82 / 0.09 = -2.0
0.966150.997812.50.95317.50.9978 - 0.953 = 0.04480.966 - 0.953 / 0.0448 = 0.29

你可以看到我们基本上是在内插外部余弦函数和内插余弦函数这是基于。如果你还是看不清是怎么回事,不用担心,你可以把这个公式当成理所当然,等你长大了,更聪明了,再回到这里。

我们现在有一个强度值,在聚光灯外是负值,在内锥内是高于1.0,在边缘之间的某个地方。如果我们正确地夹住这些值,我们就不需要在碎片着色器中使用If -else了,我们可以简单地用计算出来的强度值乘以光组件:


float theta     = dot(lightDir, normalize(-light.direction));
float epsilon   = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);    
...
// we'll leave ambient unaffected so we always have a little light.
diffuse  *= intensity;
specular *= intensity;
...

注意,我们使用了clamp函数,该函数将其第一个参数夹在数值0.0和1.0之间。这确保了强度值不会超出[0,1]范围。

请确保您将outerCutOff值添加到Light struct中,并在应用程序中设置其统一值。以下图像采用内截止角12.5和外截止角17.5:

啊,好多了。尝试使用内部和外部的切断角度,创造一个更适合你需要的聚光灯。您可以在这里 here. 找到应用程序的源代码。

这样的手电筒/聚光灯类型的灯非常适合恐怖游戏,结合方向灯和点灯环境将真正开始亮起来。

Exercises

  • Try experimenting with all the different light types and their fragment shaders. Try inverting some vectors and/or use < instead of >. Try to explain the different visual outcomes.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值