1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-22 20:25:28 +08:00

Rewrite 02-05

This commit is contained in:
Meow J
2017-06-23 16:17:46 +08:00
parent 3a3c59dc52
commit fd8f8bb099
3 changed files with 134 additions and 124 deletions

View File

@@ -321,7 +321,7 @@ for(unsigned int i = 0; i < 10; i++)
![coordinate_systems_multiple_objects](../img/01/08/coordinate_systems_multiple_objects.png)
完美!这就像我们的箱子找到志同道合的小伙伴一样。如果你在这里卡住了,你可以对照一下[源代码](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/6.3.coordinate_systems_multiple/coordinate_systems_multiple.cpp) 。
完美!看起来我们的箱子已经找到志同道合的小伙伴。如果你在这里卡住了,你可以对照一下[源代码](https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/6.3.coordinate_systems_multiple/coordinate_systems_multiple.cpp) 。
## 练习

View File

@@ -3,30 +3,30 @@
原文 | [Light casters](http://www.learnopengl.com/#!Lighting/Light-casters)
---|---
作者 | JoeyDeVries
翻译 | [Django](http://bullteacher.com/)
校对 | [Geequlim](http://geequlim.com), [BLumia](https://github.com/BLumia)
翻译 | Meow J
校对 | 暂未校对
我们目前使用的所有光照都来自于一个单独的光源,这是空间中的一个点。它的效果不错,但是在真实世界,我们有多种类的光,它们每个表现都不同。一个光源把光投射到物体上,叫做投光。这个教程里我们讨论几种不同的投光类型。学模拟不同的光源是你未来丰富你的场景的另一个工具。
我们目前使用的光照都来自于空间中的一个点。它能给我们不错的效果,但现实世界,我们有多种类的光照,每种的表现都不同。将光**投射**(Cast)到物体的光源叫做<def>投光物</def>(Light Caster)。在这一节中,我们将会讨论几种不同类型的投光。学模拟不同种类的光源是又一个能够进一步丰富场景的工具。
我们首先讨论定向光(directional light),接着是作为之前学到知识的扩展的点光(point light),最后我们讨论聚光(Spotlight)。下面的教程我们会把这几种不同的光类型整合到一个场景中。
我们首先将会讨论定向光(Directional Light),接下来是点光源(Point Light)它是我们之前学习的光源的拓展,最后我们将会讨论聚光(Spotlight)。在[下一节](06 Multiple lights.md)中我们将讨论如何将这些不同种类的光类型整合到一个场景中。
# 定向
# 平行
当一个光源很远的时,来自光源的每条光线接近于平行。这看起来像所有的光线来自于同一个方向,无论物体和观察者在哪儿。当一个光源被设置为无限远时,它被称为定向光(Directional Light),因为所有光线都有着同一个方向;它会独立于光源的位置
当一个光源处于很远的地方时,来自光源的每条光线就会近似于互相平行。不论物体和/或者观察者的位置,看起来像所有的光来自于同一个方向。当我们使用一个假设光源处于**无限**远处的模型时,它被称为<def>定向光</def>,因为它的所有光线都有着相同的方向,它与光源的位置是没有关系的
我们知道的定向光源的一个例子是太阳。太阳我们不是无限远,但它也足够远了,在计算光照的时候,我们感觉它就像无限远。在下面的图片里,来自太阳的所有光线都被定义为平行光
定向光非常好的一个例子是太阳。太阳距离我们不是无限远,但它已经远到在光照计算中可以把它视为无限远了。所以来自太阳的所有光线将被模拟为平行光线,我们可以在下图看到
![](../img/02/05/light_casters_directional.png)
因为所有的光线都是平行的,对场景中每个物体光的方向都保持一致,物体和光源的位置保持怎样的关系都无所谓。由于光的方向向量保持一致,光照计算会和场景中的其他物体相似
因为所有的光线都是平行的,所以物体与光源的相对位置是不重要的,因为对场景中每个物体光的方向都是一致的。由于光的位置向量保持一致,场景中每个物体的光照计算将会是类似的
我们可以通过定义一个光方向向量来模拟这样一个定向光,而不是使用光的位置向量。着色器计算保持大致相同的要求,这次我们直接使用光的方向向量来代替用`lightDir`向量和`position`向量的计算:
我们可以定义一个光线方向向量而不是位置向量来模拟一个定向光。着色器计算基本保持不变,但这次我们直接使用光的<var>direction</var>向量而不是通过<var>direction</var>来计算<var>lightDir</var>向量。
```c++
struct Light
{
// vec3 position; // 现在不在需要光源位置了,因为它是无限远的
struct Light {
// vec3 position; // 使用定向光就不再需要了
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
@@ -39,88 +39,85 @@ void main()
}
```
注意我们首先对`light.direction`向量取反。目前我们使用的光照计算需要光的方向作为一个来自片段朝向的光源的方向,但人们通常更习惯定义一个定向光为一个全局方向,它从光源发出。所以我们必须对全局光方向向量取反来改变它的方向它现在是一个方向向量指向光源。同时,确保对向量进行标准化处理,因为假定输入向量就是一个单位向量是不明智的。
注意我们首先对<var>light.direction</var>向量取反。我们目前使用的光照计算需求一个从片段**至**光源的光线方向,但人们更习惯定义定向光为一个**从**光源出发的全局方向。所以我们需要对全局光方向向量取反来改变它的方向它现在是一个指向光源的方向向量了。而且,记得对向量进行标准化,假设输入向量一个单位向量是不明智的。
作为结果的`lightDir`向量被使用在`diffuse`和`specular`计算之前
最终的<var>lightDir</var>向量将和以前一样用在漫反射和镜面光计算中
为了清晰地强调一个定向光对所有物体都有同样的影响,我们再次访问[坐标系教程](../01 Getting started/08 Coordinate Systems.md)结尾部分的箱子场景。例子里我们先定义10个不同的箱子位置,为每个箱子生成不同的模型矩阵,每个模型矩阵包含相应的本地到世界变换:
为了清楚地展示定向光对多个物体具有相同的影响,我们将会再次使用[坐标系](../01 Getting started/08 Coordinate Systems.md)章节最后的那个箱子派对的场景。如果你错过了派对,我们先定义了十个不同的[箱子位置](https://learnopengl.com/code_viewer.php?code=lighting/light_casters_container_positions),并对每个箱子生成了一个不同的模型矩阵,每个模型矩阵包含了对应的局部-世界坐标变换:
```c++
for(GLuint i = 0; i < 10; i++)
for(unsigned int i = 0; i < 10; i++)
{
model = glm::mat4();
glm::mat4 model;
model = glm::translate(model, cubePositions[i]);
GLfloat angle = 20.0f * i;
model = glm::rotate(model, angle, glm::vec3(1.0f, 0.3f, 0.5f));
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
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);
}
```
同时,不要忘记定义光源的方向(注意我们方向定义为:从光源处发出的方向;在下面,你可以快速看到光的方向的指向):
同时,不要忘记定义光源的方向(注意我们方向定义为**从**光源出发的方向,你可以很容易看到光的方向朝下)。
```c++
GLint lightDirPos = glGetUniformLocation(lightingShader.Program, "light.direction");
glUniform3f(lightDirPos, -0.2f, -1.0f, -0.3f);
lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);
```
!!! Important
我们已经把光的位置和方向向量传递为vec3但是有些人去想更喜欢所有的向量设置为vec4.当定义位置向量为vec4的时候把w元素设置为1.0非常重要,这样平移和投影才会合理的被应用。然而当定义一个方向向量为vec4,我们不想让平移发挥作用因为它们除了代表方向其他什么也不是所以我们把w元素设置为0.0。
我们一直将光的位置和位置向量定义为`vec3`,但一些人会喜欢所有的向量都定义为`vec4`。当我们将位置向量定义为一个`vec4`时很重要的一点是要将w分量设置为1.0,这样变换和投影才能正确应用。然而,当我们定义一个方向向量为`vec4`的时候,我们不想让位移有任何的效果因为它仅仅代表的是方向所以我们将w分量设置为0.0。
方向向量被表示为:vec4(0.2f, 1.0f, 0.3f, 0.0f)。这可以作为简单检查光的类型的方法:你可以检查w元素是否等于1.0查看我们现在所拥有的光的位置向量w是否等于0.0我们有一个光的方向向量,所以根据那个调整计算方法
方向向量就会像这样来表示:`vec4(0.2f, 1.0f, 0.3f, 0.0f)`。这可以作为一个快速检测光照类型的工具:你可以检测w分量是否等于1.0来检测它是否是光的位置向量w分量等于0.0则它是光的方向向量,这样就能根据这个来调整光照计算了
if(lightVector.w == 0.0) // 请留意浮点数错误
if(lightVector.w == 0.0) // 注意浮点数据类型的误差
// 执行定向光照计算
else if(lightVector.w == 1.0)
// 像上一个教程一样执行顶点光照计算
// 根据光源的位置做光照计算(与上一节一样)
你知道吗这正是旧OpenGL固定函数式决定光源是定向光还是位置光源(Positional Light Source)的方法,并根据它来调整光照。
有趣的事实这就是旧OpenGL固定函数式决定一个光源是一个定向光还是位置光源根据这个修改它的光照。
如果你现在编译应用,并在场景中自由移动,它看起来像有一个太阳一样的光源,把光抛到物体身上。你可以看到`diffuse`和`specular`元素都对该光源进行反射了,就像天空上有一个光源吗?看起来就像这样:
如果你现在编译程序,在场景中自由移动,你就可以看到好像有一个太阳一样的光源对所有的物体投光。你能注意到漫反射和镜面光分量的反应都好像在天空中有一个光源的感觉吗?它会看起来像这样:
![](../img/02/05/light_casters_directional_light.png)
你可以在这里获得[应用的所有代码](http://learnopengl.com/code_viewer.php?code=lighting/light_casters_directional),这里是[顶点](http://learnopengl.com/code_viewer.php?code=lighting/lighting_maps&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=lighting/light_casters_directional&type=fragment)着色器代码。
你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/5.1.light_casters_directional/light_casters_directional.cpp)找到程序的所有代码。
# 点光源
定向光作为全局光可以照亮整个场景,这非常棒,但是另一方面除了定向光,我们通常也需要几个点光源(Point Light),在场景里发亮。点光是一个在时间里有位置的光源,它所有方向发光,光线随距离增加逐渐变暗。想象灯泡和火炬作为投光物,它们可以扮演点光的角色
定向光对于照亮整个场景的全局光源是非常棒,但除了定向光之外我们也需要一些分散在场景中的<def>点光源</def>(Point Light)。点光源是处于世界中某一个位置的光源,它会朝着所有方向发光,光线会随着距离逐渐衰减。想象作为投光物的灯泡和火把,它们都是点光源
![](../img/02/05/light_casters_point.png)
之前的教程我们已经使用了(最简单的)点光。我们指定了一个光源以及其所在的位置,它从这个位置向所有方向散光线。然而,我们定义的光源模拟光线的强度却不会因为距离变远而衰减,这使得看起来像是光源亮度强。在大多数3D仿真场景中,我们希望模拟一个仅仅能照亮靠近光源附近场景的光源,而不是照亮整个场景的光源
之前的教程中,我们一直都在使用一个(简化的)点光。我们在给定位置有一个光源,它会从它的光源位置开始朝着所有方向散光线。然而,我们定义的光源模拟的是永远不会衰减的光线,这看起来像是光源亮度非常的强。在大部分的3D模拟中,我们希望模拟的光源仅照亮光源附近的区域而不是整个场景。
如果你10个箱子添加到之前教程的光照场景中,你会注意到黑暗中的每个箱子都会有同样的亮度,就像箱子在光照的前面;没有公式定义光的距离衰减。我们想让黑暗中与光源比较近的箱子被轻微地照亮。
如果你10个箱子加入到上一节光照场景中,你会注意到在最后面的箱子和在灯面前的箱子都以相同的强度被照亮,并没有定义一个公式来将光随距离衰减。我们希望在后排的箱子与前排的箱子相比仅仅是被轻微地照亮。
## 衰减
随着光线穿越距离的变远使得亮度也相应地减少的现象,通常称之为**衰减(Attenuation)**。一种随着距离减少亮度的方式是使用线性等式。这样的一个随着距离减少亮度的线性方程,可以使远处的物体更暗。然而,这样的线性方程效果会有点假。在实世界,通常光在近处时非常亮,但是一个光源的亮度开始的时候减少的非常快,之后随着距离的增加,减少的速度会慢下来。我们需要一不同的方程来减少光的度。
随着光线传播距离的增长逐渐削减光的强度通常叫做<def>衰减</def>(Attenuation)。随距离减少光强度的一种方式是使用一个线性方程。这样的方程能够随着距离的增长线性地减少光的强度,从而让远处的物体更暗。然而,这样的线性方程通常会看起来比较假。在实世界中,灯在近处通常会非常亮,但随着距离的增加光源的亮度开始会下降非常快,但在远处时剩余的光强度就会下降的非常缓慢了。所以,我们需要一不同的公式来减少光的度。
幸运的是一些聪明人已经早就把它想到了。下面的方程把一个片段的光的亮度除以一个已经计算出来的衰减值,这个值根据光源的远近得到
幸运的是一些聪明人已经帮我们解决了这个问题。下面这个公式根据片段距光源的距离计算了衰减值,之后我们会将它乘以光的强度向量
$$
\begin{equation} F_{att} = \frac{1.0}{K_c + K_l * d + K_q * d^2} \end{equation}
$$
在这里\(d\)代表片段光源的距离。为了计算衰减值我们定义3个可配置**常数**项\(K_c\)**一次**项\(K_l\)和**二次**项\(K_q\)。
在这里\(d\)代表片段光源的距离。接下来为了计算衰减值我们定义3个可配置)项:<def>常数</def>项\(K_c\)、<def>一次</def>项\(K_l\)和<def>二次</def>项\(K_q\)。
- 常数项通常1.0它的作用是保证分母永远不会比1小因为它可以利用一定的距离增加度,这个结果不会影响到我们所寻找的
- 一次项用于与距离值相乘,这会以线性的方式减少度。
- 二次项用于与距离的平方相乘,光源设置一个亮度的二次递减。二次项在距离比较的时候相比一次项会比一次项小,但当距离更远的时候比一次项更大。
- 常数项通常保持为1.0,它的主要作用是保证分母永远不会比1小否则的话在某些距离上它反而会增加度,这肯定不是我们想要的效果
- 一次项与距离值相乘,以线性的方式减少度。
- 二次项与距离的平方相乘,光源以二次递减的方式减少强度。二次项在距离比较的时候影响会比一次项小很多,但当距离值比较大的时候它就会比一次项更大
由于二次项的光会以线性方式减少,指导距离足够大的时候,就会超过一次项,之后,光的度会减少的更快。最后的效果就是光在近距离时亮度很高,但距离变远亮度迅速降低,最后亮度降低速度再次变慢。下面的图展示了在100以内的范围这样的衰减效果
由于二次项的存在,光线会在大部分时候以线性方式衰退,直到距离变得足够大,让二次项超过一次项,光的度会以更快的速度下降。这样的结果就是光在近距离时亮度很高,但随着距离变远亮度迅速降低,最后会以更慢的速度减少亮度。下面这张图显示了在100的距离内衰减效果
![](../img/02/05/attenuation.png)
你可以看到当距离很近的时候光有最强的亮度,但随着距离增大,亮度明显减弱,大约接近100的时候就会慢下来。这是我们想要的。
你可以看到光在近距离的时候有着最高的强度,但随着距离增长,它的强度明显减弱,并缓慢地在距离大约100的时候强度接近0。这是我们想要的。
### 选择正确的值
但是,我们把这三个项设置什么值呢?正确的值的设置由很多因素决定:环境、希望光覆盖的距离范围、光的类型等。大多数场合,这是经验的问题,也要适度调整。下面的表格展示一些各项的值,它们模拟现实(某种类型的)光源,覆盖特定半径(距离)。第一栏定义一个光的距离它覆盖所给定的项。这些值是大多数光的良好开始它是来自Ogre3D的维基的礼物
但是,该对这三个项设置什么值呢?正确地设定它们的值取决于很多因素:环境、希望光覆盖的距离、光的类型等。大多数情况下,这是经验的问题,以及适量的调整。下面这个表格显示了模拟一个(大概)真实的,覆盖特定半径(距离)的光源时,这些项可能取的一些值。第一列指定的是在给定的三项时光所能覆盖的距离。这些值是大多数光源很好的起始点,它们由[Ogre3D的Wiki](http://www.ogre3d.org/tikiwiki/tiki-index.php?page=-Point+Light+Attenuation)所提供
距离|常数项|一次项|二次项
-------|------|-----|------
@@ -137,47 +134,49 @@ $$
600|1.0|0.007|0.0002
3250|1.0|0.0014|0.000007
就像你所看到,常数项\(K_c\)一直都是1.0。一次项\(K_l\)为了覆盖更远的距离通常很小,二次项\(K_q\)更小。尝试这些值进行实验,看看它们在你的实现中各自的效果。我们的环境中32到100的距离对大多数光通常就足够了。
你可以看到,常数项\(K_c\)在所有的情况下都是1.0。一次项\(K_l\)为了覆盖更远的距离通常很小,二次项\(K_q\)甚至更小。尝试这些值进行实验,看看它们在你的实现中有什么效果。我们的环境中32到100的距离对大多数的光源都足够了。
### 实现衰减
为了实现衰减,在着色器中我们需要三个额外值:也就是公式的常、一次项和二次项。最好把它们储存在之前定义的Light结构体中。注意的是我们计算`lightDir`,就是在前面的教程中我们所做的,不是像之前的定向光的那部分。
为了实现衰减,在片段着色器中我们需要三个额外值:也就是公式的常数项、一次项和二次项。它们最好储存在之前定义的<fun>Light</fun>结构体中。注意我们使用上一节中计算<var>lightDir</var>的方法,而不是上面**定向光**部分
```c++
struct Light
{
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
```
然后我们在OpenGL中设置这些项我们希望光覆盖50的距离所以我们会使用上面的表格中合适的常数项、一次项和二次项:
然后我们在OpenGL中设置这些项我们希望光源能够覆盖50的距离所以我们会使用表格中对应的常数项、一次项和二次项:
```c++
glUniform1f(glGetUniformLocation(lightingShader.Program, "light.constant"), 1.0f);
glUniform1f(glGetUniformLocation(lightingShader.Program, "light.linear"), 0.09);
glUniform1f(glGetUniformLocation(lightingShader.Program, "light.quadratic"), 0.032);
lightingShader.setFloat("light.constant", 1.0f);
lightingShader.setFloat("light.linear", 0.09f);
lightingShader.setFloat("light.quadratic", 0.032f);
```
在片段着色器中实现衰减直接:我们根据公式简单的计算衰减值,在乘以`ambient`、`diffuse`和`specular`元素
在片段着色器中实现衰减还是比较直接:我们根据公式计算衰减值,之后再分别乘以环境光、漫反射和镜面光分量
我们需要光源的距离提供给公式;还记得我们是怎计算向量的长度吗?我们可以通过获取片段和光源之间的不同向量把向量的长度结果作为距离项。我们可以使用GLSL内建`length`函数做这件事
我们需要公式中距光源的距离还记得我们是怎计算一个向量的长度吗?我们可以通过获取片段和光源之间的向量差,并获取结果向量的长度作为距离项。我们可以使用GLSL内建的<fun>length</fun>函数来完成这一点
```c++
float distance = length(light.position - FragPos);
float attenuation = 1.0f / (light.constant + light.linear*distance +light.quadratic*(distance*distance));
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
```
然后,我们在光照计算中,通过把衰减值乘以`ambient`、`diffuse`和`specular`颜色,包含这个衰减值
接下来,我们将包含这个衰减值到光照计算中,将它分别乘以环境光、漫反射和镜面光颜色
!!! Important
我们可以可以把`ambient`元素留着不变,这样`amient`光照不会随着距离减少,但是如果我们使用多余1个的光源,所有的`ambient`元素会开始叠加,因此这种情况我们希望`ambient`光照也衰减。简单的调试出对于你的环境来说最好的效果
我们可以将环境光分量保持不变,让环境光照不会随着距离减少,但是如果我们使用多于一个的光源,所有的环境光分量将会开始叠加,所以在这种情况我们希望衰减环境光照。简单实验一下,看看什么才能在你的环境中效果最好
```c++
ambient *= attenuation;
@@ -185,39 +184,37 @@ diffuse *= attenuation;
specular *= attenuation;
```
如果你运行应用后获得这样的果:
如果你运行程序的话,你会获得这样的果:
![](../img/02/05/light_casters_point_light.png)
你可以看到现在只有最近处的箱子的前面被照得最亮。后的箱子一点都没照亮,因为它们离光源太远了。你可以在这里找到[应用源码](http://learnopengl.com/code_viewer.php?code=lighting/light_casters_point)和[片段着色器](http://learnopengl.com/code_viewer.php?code=lighting/light_casters_point&type=fragment)的代码。
你可以看到,只有前排的箱子被照亮的,距离最近的箱子是最亮。后的箱子一点都没照亮,因为它们离光源实在是太远了。你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/5.2.light_casters_point/light_casters_point.cpp)找到程序的代码。
点光就是一个可配的置位置和衰减值应用到光照计算中。还有另一种类型光可用于我们照明库当中
点光就是一个能够配置位置和衰减的光源。它是我们光照工具箱中的又一个光照类型
# 聚光
## 聚光
我们要讨论的最后一种类型的光是<def>聚光</def>(Spotlight)。聚光是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。这样的结果就是只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗。聚光很好的例子就是路灯或手电筒。
我们要讨论的最后一种类型光是聚光(Spotlight)。聚光是一种位于环境中某处的光源,它不是向所有方向照射,而是只朝某个方向照射。结果是只有一个聚光照射方向的确定半径内的物体才会被照亮,其他的都保持黑暗。聚光的好例子是路灯或手电筒。
OpenGL中的聚光用世界空间位置一个方向和一个指定了聚光半径的切光角来表示。我们计算的每个片段如果片段在聚光的切光方向之间就是在圆锥体内我们就会把片段照亮。下面的图可以让你明白聚光是如何工作的
OpenGL中聚光是用一个世界空间位置、一个方向和一个<def>切光</def>角(Cutoff Angle)来表示的,切光角指定了聚光的半径(译注:是圆锥的半径不是距光源距离那个半径)。对于每个片段,我们会计算片段是否位于聚光的切光方向之间(也就是在锥形内),如果是的话,我们就会相应地照亮片段。下面这张图会让你明白聚光是如何工作的:
![](../img/02/05/light_casters_spotlight_angles.png)
* `LightDir`:从片段指向光源的向量。
* `SpotDir`:聚光所指向的方向。
* `Phi`\(\phi\)定义聚光半径的切光角。每个落在这个角度之外的,聚光都不会照亮。
* `Theta`\(\theta\)`LightDir`向量和`SpotDir`向量之间的角度。\(\theta\)值应该比\(\Phi\)值小,这样才会在聚光内
- `LightDir`:从片段指向光源的向量。
- `SpotDir`:聚光所指向的方向。
- `Phi`\(\phi\)指定了聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。
- `Theta`\(\theta\)<var>LightDir</var>向量和<var>SpotDir</var>向量之间的夹角。在聚光内部的话\(\theta\)值应该比\(\phi\)值小。
所以我们大致要做的是,计算`LightDir`向量和`SpotDir`向量的点乘(返回两个单位向量的点乘,还记得吗?),然后在和切光角\(\phi\)对比。现在应该明白聚光是我们下面将创建的手电筒的范例
所以我们要做的就是计算<var>LightDir</var>向量和<var>SpotDir</var>向量之间的点积(还记得它会返回两个单位向量夹角的余弦值吗?),并将它与切光角\(\phi\)对比。现在应该了解聚光究竟是什么了,下面我们将以手电筒的形式创建一个聚光
## 手电筒
手电筒(Flashlight)是一个坐落在观察者位置的聚光,通常瞄准玩家透视图的前面。基本上说,一个手电筒是一个普通的聚光,但是根据玩家的位置和方向持续的更新它的位置和方向
手电筒(Flashlight)是一个位于观察者位置的聚光,通常它都会瞄准玩家视角的正前方。基本上说,手电筒是普通的聚光,但的位置和方向会随着玩家的位置和朝向不断更新
所以我们需要为片段着色器提供的值,是聚光的位置向量(来计算光的方向坐标),聚光的方向向量和切光角。我们可以把这些值储存在`Light`结构体中:
所以,在片段着色器中我们需要的值有聚光的位置向量(来计算光的方向向量)、聚光的方向向量和一个切光角。我们可以将它们储存在<fun>Light</fun>结构体中:
```c++
struct Light
{
struct Light {
vec3 position;
vec3 direction;
float cutOff;
@@ -225,64 +222,64 @@ struct Light
};
```
下面我们把这些适当的值传着色器:
接下来我们将合适的值传着色器
```c++
glUniform3f(lightPosLoc, camera.Position.x, camera.Position.y, camera.Position.z);
glUniform3f(lightSpotdirLoc, camera.Front.x, camera.Front.y, camera.Front.z);
glUniform1f(lightSpotCutOffLoc, glm::cos(glm::radians(12.5f)));
lightingShader.setVec3("light.position", camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
lightingShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
```
你可以看到,我们切光角设置一个角度,但是我们根据一个角度计算了余弦值,把这个余弦结果传给了片段着色器。这做的原因是在片段着色器中,我们计算`LightDir`和`SpotDir`向量的点乘,而点乘返回一个余弦值不是一个角度,所以我们不能直接把一个角度和余弦值对比。为了获得这个角度我们必须计算点结果的反余弦,这个操作开销很大的。所以为了节约一性能,我们先计算给定切光角的余弦值,然后把结果传递给片段着色器。由于个角度都被表示为余弦了,我们可以直接对它们而不用进行任何开销高昂的操作
你可以看到,我们并没有给切光角设置一个角度值,反而是用角度计算了一个余弦值,余弦结果传递到片段着色器。这做的原因是在片段着色器中,我们计算`LightDir`和`SpotDir`向量的点积,这个点积返回的将是一个余弦值不是角度,所以我们不能直接使用角度和余弦值进行比较。为了获角度我们需要计算点结果的反余弦,这是一个开销很大的计算。所以为了节约一性能开销,我们将会计算切光角对应的余弦值,并将它的结果传片段着色器。由于这两个角度现在都由余弦角来表示了,我们可以直接对它们进行比较而不用进行任何开销高昂的计算
现在剩下要做的是计算\(\theta\)值,用它和\(\phi\)对比,决定我们是否在或不在聚光的内部:
接下来就是计算\(\theta\)值,并将它和切光角\(\phi\)对比,决定是否在聚光的内部:
```c++
float theta = dot(lightDir, normalize(-light.direction));
if(theta > light.cutOff)
{
// 执行光照计算
}
else // 否则使用环境光,使得场景不至于完全黑暗
color = vec4(light.ambient*vec3(texture(material.diffuse,TexCoords)), 1.0f);
else // 否则使用环境光,让场景在聚光之外时不至于完全黑暗
color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);
```
我们首先计算`lightDir`和取反的`direction`向量的点乘(它是取反过的因为我们想向量指向光源而不是从光源作为指向出发点。译注:前面的`specular`教程中作者却用了相反的表示方法,这里读者可以选择喜欢的表达方式)。确保对所有相关向量进行了标准化处理
我们首先计算了<var>lightDir</var>和取反的<var>direction</var>向量(取反的是因为我们想向量指向光源而不是从光源出发)之间的点积。记住要对所有相关向量标准化。
!!! Important
你可能奇怪为什么if条件中使用>符号而不是<符号。为了在聚光以内,`theta`不是应该比光的切光更小吗?这没错,但不要忘了,角度值是以余弦值来表示的一个0度的角表示1.0的余弦值,一个角是90度的时候被表示为0.0的余弦值,你可以在这里看到:
你可能奇怪为什么if条件中使用的是 > 符号而不是 < 符号。<var>theta</var>不应该比光的切光更小才是在聚光内部吗?这并没有错,但不要忘角度值现在都由余弦值来表示的一个0度的角表示的是1.0的余弦值,一个90度的角度表示的是0.0的余弦值,你可以在下图中看到:
![](../img/02/05/light_casters_cos.png)
现在可以看到,余弦越接近1.0,角度就越小。这就解释了为什么θ需要比切光值更大了。切光值当前被设置为12.5的余弦,等于0.9978,所以θ的余弦值在0.99791.0之间,片段在聚光内,被照亮。
现在可以看到,余弦越接近1.0它的角度就越小。这就解释了为什么<var>theta</var>要比切光值更大了。切光值目前设置为12.5的余弦,等于0.9978所以在0.99791.0内的<var>theta</var>值才能保证片段在聚光内,从而被照亮。
运行应用,在聚光内的片段才会被照亮。这看起来像这样:
运行程序,你将会看到一个聚光,它仅会照亮聚光圆锥内的片段。看起来像这样
![](../img/02/05/light_casters_spotlight_hard.png)
你可以在这里获得[全部源码](http://learnopengl.com/code_viewer.php?code=lighting/light_casters_spotlight_hard)和[片段着色器的源码](http://learnopengl.com/code_viewer.php?code=lighting/light_casters_spotlight_hard&type=fragment)
你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/5.3.light_casters_spot/light_casters_spot.cpp)获得全部源码
它看起来仍然有点假,原因是聚光有了一个硬边。片段着色器一旦到达了聚光圆锥边缘,它就立刻黑了下来,却没有任何平滑减弱的过。一个真实的聚光的光会在它的边界处平滑减弱的
但这仍看起来有些假,主要是因为聚光有一圈硬边。当一个片段遇到聚光圆锥边缘,它会完全变暗,没有一点平滑的过。一个真实的聚光将会在边缘处逐渐减少亮度
## 平滑/软化边缘
为创建聚光的平滑边,我们希望去模拟聚光有一个内圆锥和外圆锥。我们可以内圆锥设置为前面部分定义的圆锥,我们希望外圆锥从内边到外边逐步的变暗
创建一种看起来边缘平滑的聚光,我们需要模拟聚光有一个<def>内</def>圆锥(Inner Cone)和一个<def>外</def>圆锥(Outer Cone)。我们可以内圆锥设置为上一部分中的那个圆锥,我们也需要一个外圆锥,来让光从内圆锥逐渐减暗,直到外圆锥的边界
为创建外圆锥,我们简单定义一个余弦值,它代表聚光方向向量和外圆锥向量(等于它的半径)的角。然后,如果片段在内圆锥和外圆锥之间,会给它计算出一个0.0到1.0之间的亮度。如果片段在内圆锥以内这个亮度就等于1.0,如果在外就是0.0。
创建一个外圆锥,我们只需要再定义一个余弦值代表聚光方向向量和外圆锥向量(等于它的半径)的角。然后,如果一个片段处于内外圆锥之间,会给它计算出一个0.0到1.0之间的强度值。如果片段在内圆锥之内,它的强度就是1.0,如果在外圆锥之外强度值就是0.0。
我们可以使用下面公式计算这样的值:
我们可以用下面这个公式计算这值:
$$
\begin{equation} I = \frac{\theta - \gamma}{\epsilon} \end{equation}
$$
这里\(\epsilon\)是内\(\phi\))和外圆锥(\(\gamma\)\epsilon = \phi - \gamma)的差。结果\(I\)的值是聚光在当前片段的亮度。
这里\(\epsilon\)(Epsilon)是内(\(\phi\))和外圆锥(\(\gamma\)之间的余弦值差(\(\epsilon = \phi - \gamma\))。最终的\(I\)值就是在当前片段聚光的强度。
很难用图画描述出这个公式是怎工作的,所以我们尝试使用一个例子
很难去表现这个公式是怎工作的,所以我们用一些实例值来看看
\(\theta\)|\(\theta\)(角度)|\(\phi\)(内切)|\(\phi\)(角度)|\(\gamma\)(外切)|\(\gamma\)(角度)|\(\epsilon\)|\(I\)
\(\theta\)|\(\theta\)(角度)|\(\phi\)(内光切)|\(\phi\)(角度)|\(\gamma\)(外光切)|\(\gamma\)(角度)|\(\epsilon\)|\(I\)
--|---|---|---|---|---|---|---
0.87|30|0.91|25|0.82|35|0.91 - 0.82 = 0.09|0.87 - 0.82 / 0.09 = 0.56
0.9|26|0.91|25|0.82|35|0.91 - 0.82 = 0.09|0.9 - 0.82 / 0.09 = 0.89
@@ -291,31 +288,31 @@ $$
0.64|50|0.91|25|0.82|35|0.91 - 0.82 = 0.09|0.64 - 0.82 / 0.09 = -2.0
0.966|15|0.9978|12.5|0.953|17.5|0.966 - 0.953 = 0.0448|0.966 - 0.953 / 0.0448 = 0.29
就像你看到的那样我们基本是根据θ在外余弦和内余弦之间插值。如果你仍不明白怎么继续,不担心。你可以简单的使用这个公式计算,当你更加老道和明白的时候再看。
你可以看到,我们基本是在内外余弦值之间根据\(\theta\)插值。如果你仍不明白发生了什么,不担心,只需要记住这个公式就好了,在你更聪明的时候再回来看看。
由于我们现在有了一个亮度值,当在聚光外的时候是个负的,在内圆锥内大于1。如果我们适当地把这个值固定,我们在片段着色器中就再不需要if-else了我们可以简单地用计算出的亮度值乘以光的元素
我们现在有了一个在聚光外是负的在内圆锥内大于1.0的,在边缘处于两者之间的强度值了。如果我们正确地约束(Clamp)这个值,在片段着色器中就不再需要`if-else`了,我们能够使用计算出来的强度值直接乘以光照分量
```c++
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
...
// Well leave ambient unaffected so we always have a little
light.diffuse* = intensity;
// 将不对环境光做出影响,让它总是能有一点光
diffuse *= intensity;
specular *= intensity;
...
```
注意我们使用了`clamp`函数,它把第一个参数固定在0.01.0之间。这保证了亮度值不会超出[0, 1]外。
注意我们使用了<fun>clamp</fun>函数,它把第一个参数约束(Clamp)在了0.01.0之间。这保证度值不会[0, 1]区间之外。
确定你把`outerCutOff`值添加到了`Light`结构体,并在应用中设置它的uniform值。对于下面的图片,内部切光角`12.5f`,外切光角是`17.5f`
确定你<var>outerCutOff</var>值添加到了<fun>Light</fun>结构体之中,并在程序中设置它的uniform值。下面的图片中,我们使用的内切光角12.5外切光角是17.5
![](../img/02/05/light_casters_spotlight.png)
看起来好多了。仔细看看内部和外部切光角,尝试创建一个符合你求的聚光。可以在这里找到应用源码,以及片段的源码。
啊,这样看起来好多了。稍微对内外切光角实验一下,尝试创建一个更能符合你求的聚光。可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/5.4.light_casters_spot_soft/light_casters_spot_soft.cpp)找到程序的源码。
这样的一个手电筒/聚光类型的灯光非常适合恐怖游戏,结合定向和点光,环境会真的开始被照亮了。[下一节](06 Multiple lights.md)中,我们会结合所有我们目前讨论了的光和技巧。
这样的手电筒/聚光类型的灯光非常适合恐怖游戏,结合定向和点光,环境会开始被照亮了。[下一节](06 Multiple lights.md)的教程中,我们会结合我们至今讨论的所有光照和技巧。
## 练习
- 试着修改上面的各种不同种类的光源及其片段着色器。试着将部分矢量进行反向并尝试使用 < 来代替 > 。试着解释这些修改导致不同显示效果的原因。
- 尝试实验一下上面的所有光照类型和它们的片段着色器。试着对一些向量进行取反,并使用 < 来代替 >。试着解释不同视觉效果产生的原因。

View File

@@ -225,6 +225,19 @@
- Emission Map放射光贴图
- Emission Value发光值
## 02-05
- Cast投射
- Light Caster投光物
- Directional Light定向光
- Point Light点光源
- Spotlight聚光
- Positional Light Source位置光源
- Fixed-functionality (OpenGL):固定函数式
- Attenuation衰减
- Cutoff Angle切光角
- Flashlight手电筒
## 06-01
- Debugging调试