From 689e936dc732d4ed4dc40c175096e7668bc65f10 Mon Sep 17 00:00:00 2001 From: Meow J Date: Mon, 19 Jun 2017 03:31:22 +0800 Subject: [PATCH] Update 02-02 --- docs/02 Lighting/02 Basic Lighting.md | 174 ++++++++++---------- docs/img/02/02/basic_lighting_exercise3.png | Bin 0 -> 20727 bytes glossary.md | 24 +++ mkdocs.yml | 2 +- 4 files changed, 113 insertions(+), 87 deletions(-) create mode 100644 docs/img/02/02/basic_lighting_exercise3.png diff --git a/docs/02 Lighting/02 Basic Lighting.md b/docs/02 Lighting/02 Basic Lighting.md index 6dbde66..5c82318 100644 --- a/docs/02 Lighting/02 Basic Lighting.md +++ b/docs/02 Lighting/02 Basic Lighting.md @@ -1,4 +1,4 @@ -# 光照基础 +# 基础光照 原文 | [Basic Lighting](http://learnopengl.com/#!Lighting/Basic-Lighting) ---|--- @@ -6,97 +6,99 @@ 翻译 | [Django](http://bullteacher.com/) 校对 | Geequlim, [BLumia](https://github.com/blumia/) -现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是以目前我们所拥有的处理能力无法模拟的。因此OpenGL的光照仅仅使用了简化的模型并基于对现实的估计来进行模拟,这样处理起来会更容易一些,而且看起来也差不多一样。这些光照模型都是基于我们对光的物理特性的理解。其中一个模型被称为冯氏光照模型(Phong Lighting Model)。冯氏光照模型的主要结构由3个元素组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。这些光照元素看起来像下面这样: +现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是我们有限的计算能力所无法模拟的。因此OpenGL的光照使用的是简化的模型,对现实的情况进行近似,这样处理起来会更容易一些,而且看起来也差不多一样。这些光照模型都是基于我们对光的物理特性的理解。其中一个模型被称为冯氏光照模型(Phong Lighting Model)。冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。下面这张图展示了这些光照分量看起来的样子: ![](../img/02/02/basic_lighting_phong.png) -- 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上也仍然有一些光亮(月亮、一个来自远处的光),所以物体永远不会是完全黑暗的。我们使用环境光照来模拟这种情况,也就是无论如何永远都给物体一些颜色。 -- 漫反射光照(Diffuse Lighting):模拟一个发光物对物体的方向性影响(Directional Impact)。它是冯氏光照模型最显著的组成部分。面向光源的一面比其他面会更亮。 -- 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色,相比于物体的颜色更倾向于光的颜色。 +- 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。 +- 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。 +- 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。 -为了创建有趣的视觉场景,我们希望模拟至少这三种光照元素。我们将以最简单的一个开始:**环境光照**。 +为了创建有趣的视觉场景,我们希望模拟至少这三种光照分量。我们将以最简单的一个开始:**环境光照**。 # 环境光照 -光通常都不是来自于同一光源,而是来自散落于我们周围的很多光源,即使它们可能并不是那么显而易见。光的一个属性是,它可以向很多方向发散和反弹,所以光最后到达的地点可能并不是它所临近的直射方向;光能够像这样**反射(Reflect)**到其他表面,一个物体的光照可能受到来自一个非直射的光源影响。考虑到这种情况的算法叫做**全局照明(Global Illumination)**算法,但是这种算法既开销高昂又极其复杂。 +光通常都不是来自于同一个光源,而是来自于我们周围分散的很多光源,即使它们可能并不是那么显而易见。光的一个属性是,它可以向很多方向发散并反弹,从而能够到达不是非常直接临近的点。所以,光能够在其它的表面上**反射**,对一个物体产生间接的影响。考虑到这种情况的算法叫做全局照明(Global Illumination)算法,但是这种算法既开销高昂又极其复杂。 -因为我们不是复杂和昂贵算法的死忠粉丝,所以我们将会使用一种简化的全局照明模型,叫做环境光照(Ambient Lighting)。如你在前面章节所见,我们使用一个(数值)很小的常量(光)颜色添加进物体**片段**(Fragment,指当前讨论的光线在物体上的照射点)的最终颜色里,这看起来就像即使没有直射光源也始终存在着一些发散的光。 +由于我们现在对那种又复杂又开销高昂的算法不是很感兴趣,所以我们将会先使用一个简化的全局照明模型,即环境光照。正如你在上一节所学到的,我们使用一个很小的常量(光照)颜色,添加到物体片段的最终颜色中,这样子的话即便场景中没有直接的光源也能看起来存在有一些发散的光。 -把环境光照添加到场景里非常简单。我们用光的颜色乘以一个(数值)很小常量环境因子,再乘以物体的颜色,然后使用它作为片段的颜色: +把环境光照添加到场景里非常简单。我们用光的颜色乘以一个很小的常量环境因子,再乘以物体的颜色,然后将最终结果作为片段的颜色: ```c++ void main() { - float ambientStrength = 0.1f; + float ambientStrength = 0.1; vec3 ambient = ambientStrength * lightColor; + vec3 result = ambient * objectColor; - color = vec4(result, 1.0f); + FragColor = vec4(result, 1.0); } ``` -如果你现在运行你的程序,你会注意到冯氏光照的第一个阶段已经应用到你的物体上了。这个物体非常暗,但不是完全的黑暗,因为我们应用了环境光照(注意发光立方体没被环境光照影响是因为我们对它使用了另一个着色器)。它看起来应该像这样: +如果你现在运行你的程序,你会注意到冯氏光照的第一个阶段已经应用到你的物体上了。这个物体非常暗,但由于应用了环境光照(注意光源立方体没受影响是因为我们对它使用了另一个着色器),也不是完全黑的。它看起来应该像这样: ![](../img/02/02/ambient_lighting.png) # 漫反射光照 -环境光本身不提供最明显的光照效果,但是漫反射光照(Diffuse Lighting)会对物体产生显著的视觉影响。漫反射光使物体上与光线排布越近的片段越能从光源处获得更多的亮度。为了更好的理解漫反射光照,请看下图: +环境光照本身不能提供最有趣的结果,但是漫反射光照就能开始对物体产生显著的视觉影响了。漫反射光照使物体上与光线方向越接近的片段能从光源处获得更多的亮度。为了能够更好的理解漫反射光照,请看下图: ![](../img/02/02/diffuse_light.png) -图左上方有一个光源,它所发出的光线落在物体的一个片段上。我们需要测量这个光线与它所接触片段之间的角度。如果光线垂直于物体表面,这束光对物体的影响会最大化(译注:更亮)。为了测量光线和片段的角度,我们使用一个叫做法向量(Normal Vector)的东西,它是垂直于片段表面的一种向量(这里以黄色箭头表示),我们在后面再讲这个东西。两个向量之间的角度就能够根据点乘计算出来。 +图左上方有一个光源,它所发出的光线落在物体的一个片段上。我们需要测量这个光线是以什么角度接触到这个片段的。如果光线垂直于物体表面,这束光对物体的影响会最大化(译注:更亮)。为了测量光线和片段的角度,我们使用一个叫做法向量(Normal Vector)的东西,它是垂直于片段表面的一个向量(这里以黄色箭头表示),我们在后面再讲这个东西。这两个向量之间的角度很容易就能够通过点乘计算出来。 -你可能记得在[变换](../01 Getting started/07 Transformations.md)那一节教程里,我们知道两个单位向量的角度越小,它们点乘的结果越倾向于1。当两个向量的角度是90度的时候,点乘会变为0。这同样适用于θ,θ越大,光对片段颜色的影响越小。 +你可能记得在[变换](../01 Getting started/07 Transformations.md)那一节教程里,我们知道两个单位向量的夹角越小,它们点乘的结果越倾向于1。当两个向量的夹角为90度的时候,点乘会变为0。这同样适用于\(\theta\),\(\theta\)越大,光对片段颜色的影响就应该越小。 !!! Important - 注意,我们使用的是单位向量(Unit Vector,长度是1的向量)取得两个向量夹角的余弦值,所以我们需要确保所有的向量都被标准化,否则点乘返回的值就不仅仅是余弦值了(如果你不明白,可以复习[变换](../01 Getting started/07 Transformations.md)那一节的点乘部分)。 + 注意,为了(只)得到两个向量夹角的余弦值,我们使用的是单位向量(长度为1的向量),所以我们需要确保所有的向量都是标准化的,否则点乘返回的就不仅仅是余弦值了(见[变换](../01 Getting started/07 Transformations.md))。 -点乘返回一个标量,我们可以用它计算光线对片段颜色的影响,基于不同片段所朝向光源的方向的不同,这些片段被照亮的情况也不同。 +点乘返回一个标量,我们可以用它计算光线对片段颜色的影响。不同片段朝向光源的方向的不同,这些片段被照亮的情况也不同。 -所以,我们需要些什么来计算漫反射光照? +所以,计算漫反射光照需要什么? - 法向量:一个垂直于顶点表面的向量。 -- 定向的光线:作为光的位置和片段的位置之间的向量差的方向向量。为了计算这个光线,我们需要光的位置向量和片段的位置向量。 +- 定向的光线:作为光源的位置与片段的位置之间向量差的方向向量。为了计算这个光线,我们需要光的位置向量和片段的位置向量。 ## 法向量 -法向量(Normal Vector)是垂直于顶点表面的(单位)向量。由于顶点自身并没有表面(它只是空间中一个独立的点),我们利用顶点周围的顶点计算出这个顶点的表面。我们能够使用叉乘这个技巧为立方体所有的顶点计算出法线,但是由于3D立方体不是一个复杂的形状,所以我们可以简单的把法线数据手工添加到顶点数据中。更新的顶点数据数组可以在[这里](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting_vertex_data)找到。试着去想象一下,这些法向量真的是垂直于立方体的各个面的表面的(一个立方体由6个面组成)。 +法向量是一个垂直于顶点表面的(单位)向量。由于顶点本身并没有表面(它只是空间中一个独立的点),我们利用它周围的顶点来计算出这个顶点的表面。我们能够使用一个小技巧,使用叉乘对立方体所有的顶点计算法向量,但是由于3D立方体不是一个复杂的形状,所以我们可以简单地把法线数据手工添加到顶点数据中。更新后的顶点数据数组可以在[这里](https://learnopengl.com/code_viewer.php?code=lighting/basic_lighting_vertex_data)找到。试着去想象一下,这些法向量真的是垂直于立方体各个平面的表面的(一个立方体由6个平面组成)。 -因为我们向顶点数组添加了额外的数据,所以我们应该更新光照的顶点着色器: +由于我们向顶点数组添加了额外的数据,所以我们应该更新光照的顶点着色器: ```c++ #version 330 core -layout (location = 0) in vec3 position; -layout (location = 1) in vec3 normal; +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec3 aNormal; ... ``` -现在我们已经向每个顶点添加了一个法向量,已经更新了顶点着色器,我们还要更新顶点属性指针(Vertex Attibute Pointer)。注意,发光物使用同样的顶点数组作为它的顶点数据,然而发光物的着色器没有使用新添加的法向量。我们不会更新发光物的着色器或者属性配置,但是我们必须至少修改一下顶点属性指针来适应新的顶点数组的大小: +现在我们已经向每个顶点添加了一个法向量并更新了顶点着色器,我们还要更新顶点属性指针。注意,灯使用同样的顶点数组作为它的顶点数据,然而灯的着色器并没有使用新添加的法向量。我们不需要更新灯的着色器或者是属性的配置,但是我们必须至少修改一下顶点属性指针来适应新的顶点数组的大小: ```c++ -glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid * )0); +glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); ``` -我们只想使用每个顶点的前三个浮点数,并且我们忽略后三个浮点数,所以我们只需要把**步长**参数改成`GLfloat`尺寸的6倍就行了。 +我们只想使用每个顶点的前三个float,并且忽略后三个float,所以我们只需要把**步长**参数改成`float`大小的6倍就行了。 !!! Important - 发光物着色器顶点数据的不完全使用看起来有点低效,但是这些顶点数据已经从立方体对象载入到GPU的内存里了,所以GPU内存不是必须再储存新数据。相对于重新给发光物分配VBO,实际上却是更高效了。 + 虽然对灯的着色器使用不能完全利用的顶点数据看起来不是那么高效,但这些顶点数据已经从箱子对象载入后开始就储存在GPU的内存里了,所以我们并不需要储存新数据到GPU内存中。这实际上比给灯专门分配一个新的VBO更高效了。 -所有光照的计算需要在片段着色器里进行,所以我们需要把法向量由顶点着色器传递到片段着色器。我们这么做: +所有光照的计算都是在片段着色器里进行,所以我们需要将法向量由顶点着色器传递到片段着色器。我们这么做: ```c++ out vec3 Normal; + void main() { - gl_Position = projection * view * model * vec4(position, 1.0f); - Normal = normal; + gl_Position = projection * view * model * vec4(aPos, 1.0); + Normal = aNormal; } ``` -剩下要做的事情是,在片段着色器中定义相应的输入值: +接下来,在片段着色器中定义相应的输入变量: ```c++ in vec3 Normal; @@ -104,30 +106,29 @@ in vec3 Normal; ## 计算漫反射光照 -每个顶点现在都有了法向量,但是我们仍然需要光的位置向量和片段的位置向量。由于光的位置是一个静态变量,我们可以简单的在片段着色器中把它声明为uniform: +我们现在对每个顶点都有了法向量,但是我们仍然需要光源的位置向量和片段的位置向量。由于光源的位置是一个静态变量,我们可以简单地在片段着色器中把它声明为uniform: ```c++ uniform vec3 lightPos; ``` -然后再游戏循环中(外面也可以,因为它不会变)更新uniform。我们使用在前面教程中声明的`lightPos`向量作为光源位置: +然后在渲染循环中(渲染循环的外面也可以,因为它不会改变)更新uniform。我们使用在前面声明的lightPos向量作为光源位置: ```c++ -GLint lightPosLoc = glGetUniformLocation(lightingShader.Program, "lightPos"); -glUniform3f(lightPosLoc, lightPos.x, lightPos.y, lightPos.z); +lightingShader.setVec3("lightPos", lightPos); ``` -最后,我们还需要片段的位置(Position)。我们会在世界空间中进行所有的光照计算,因此我们需要一个在世界空间中的顶点位置。我们可以通过把顶点位置属性乘以模型矩阵(Model Matrix,只用模型矩阵不需要用观察和投影矩阵)来把它变换到世界空间坐标。这个在顶点着色器中很容易完成,所以让我们就声明一个输出(out)变量,然后计算它的世界空间坐标: +最后,我们还需要片段的位置。我们会在世界空间中进行所有的光照计算,因此我们需要一个在世界空间中的顶点位置。我们可以通过把顶点位置属性乘以模型矩阵(不是观察和投影矩阵)来把它变换到世界空间坐标。这个在顶点着色器中很容易完成,所以我们声明一个输出变量,并计算它的世界空间坐标: ```c++ -out vec3 FragPos; +out vec3 FragPos; out vec3 Normal; - + void main() { - gl_Position = projection * view * model * vec4(position, 1.0f); - FragPos = vec3(model * vec4(position, 1.0f)); - Normal = normal; + gl_Position = projection * view * model * vec4(aPos, 1.0); + FragPos = vec3(model * vec4(aPos, 1.0)); + Normal = aNormal; } ``` @@ -137,9 +138,9 @@ void main() in vec3 FragPos; ``` -现在,所有需要的变量都设置好了,我们可以在片段着色器中开始光照的计算了。 +现在,所有需要的变量都设置好了,我们可以在片段着色器中添加光照计算了。 -我们需要做的第一件事是计算光源和片段位置之间的方向向量。前面提到,光的方向向量是光的位置向量与片段的位置向量之间的向量差。你可能记得,在变换教程中,我们简单的通过两个向量相减的方式计算向量差。我们同样希望确保所有相关向量最后都转换为单位向量,所以我们把法线和方向向量这个结果都进行标准化: +我们需要做的第一件事是计算光源和片段位置之间的方向向量。前面提到,光的方向向量是光源位置向量与片段位置向量之间的向量差。你可能记得在[变换](../01 Getting started/07 Transformations.md)教程中,我们能够简单地通过让两个向量相减的方式计算向量差。我们同样希望确保所有相关向量最后都转换为单位向量,所以我们把法线和最终的方向向量都进行标准化: ```c++ vec3 norm = normalize(Normal); @@ -148,137 +149,138 @@ vec3 lightDir = normalize(lightPos - FragPos); !!! Important - 当计算光照时我们通常不关心一个向量的“量”或它的位置,我们只关心它们的方向。所有的计算都使用单位向量完成,因为这会简化了大多数计算(比如点乘)。所以当进行光照计算时,确保你总是对相关向量进行标准化,这样它们才会保证自身为单位向量。忘记对向量进行标准化是一个十分常见的错误。 + 当计算光照时我们通常不关心一个向量的模长或它的位置,我们只关心它们的方向。所以,几乎所有的计算都使用单位向量完成,因为这简化了大部分的计算(比如点乘)。所以当进行光照计算时,确保你总是对相关向量进行标准化,来保证它们是真正地单位向量。忘记对向量进行标准化是一个十分常见的错误。 -下一步,我们对`norm`和`lightDir`向量进行点乘,来计算光对当前片段的实际的散射影响。结果值再乘以光的颜色,得到散射因子。两个向量之间的角度越大,散射因子就会越小: +下一步,我们对normlightDir向量进行点乘,计算光源对当前片段实际的漫发射影响。结果值再乘以光的颜色,得到漫反射分量。两个向量之间的角度越大,漫反射分量就会越小: ```c++ float diff = max(dot(norm, lightDir), 0.0); vec3 diffuse = diff * lightColor; ``` -如果两个向量之间的角度大于90度,点乘的结果就会变成负数,这样会导致散射因子变为负数。为此,我们使用`max`函数返回两个参数之间较大的参数,从而保证散射因子不会变成负数。负数的颜色是没有实际定义的,所以最好避免它,除非你是那种古怪的艺术家。 +如果两个向量之间的角度大于90度,点乘的结果就会变成负数,这样会导致漫反射分量变为负数。为此,我们使用max函数返回两个参数之间较大的参数,从而保证漫反射分量不会变成负数。负数颜色的光照是没有定义的,所以最好避免它,除非你是那种古怪的艺术家。 -既然我们有了一个环境光照颜色和一个散射光颜色,我们把它们相加,然后把结果乘以物体的颜色,来获得片段最后的输出颜色。 +现在我们有了环境光分量和漫反射分量,我们把它们相加,然后把结果乘以物体的颜色,来获得片段最后的输出颜色。 ```c++ vec3 result = (ambient + diffuse) * objectColor; -color = vec4(result, 1.0f); +FragColor = vec4(result, 1.0); ``` 如果你的应用(和着色器)编译成功了,你可能看到类似的输出: ![](../img/02/02/basic_lighting_diffuse.png) -你可以看到使用了散射光照,立方体看起来就真的像个立方体了。尝试在你的脑中想象,通过移动正方体,法向量和光的方向向量之间的夹角增大,片段变得更暗。 +你可以看到使用了漫反射光照,立方体看起来就真的像个立方体了。尝试在你的脑中想象一下法向量,并在立方体周围移动,注意观察法向量和光的方向向量之间的夹角越大,片段就会越暗。 -如果你遇到很多困难,可以对比[完整的源代码](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting_diffuse)以及[片段着色器](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting_diffuse&type=fragment)代码。 +如果你在哪卡住了,可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/2.1.basic_lighting_diffuse/basic_lighting_diffuse.cpp)对比一下完整的源代码。 ## 最后一件事 -现在我们已经把法向量从顶点着色器传到了片段着色器。可是,目前片段着色器里,我们都是在世界空间坐标中进行计算的,所以,我们不是应该把法向量转换为世界空间坐标吗?基本正确,但是这不是简单地把它乘以一个模型矩阵就能搞定的。 +现在我们已经把法向量从顶点着色器传到了片段着色器。可是,目前片段着色器里的计算都是在世界空间坐标中进行的。所以,我们是不是应该把法向量也转换为世界空间坐标?基本正确,但是这不是简单地把它乘以一个模型矩阵就能搞定的。 -首先,法向量只是一个方向向量,不能表达空间中的特定位置。同时,法向量没有齐次坐标(顶点位置中的w分量)。这意味着,平移不应该影响到法向量。因此,如果我们打算把法向量乘以一个模型矩阵,我们就要把模型矩阵左上角的3×3矩阵从模型矩阵中移除(译注:所谓移除就是设置为0),它是模型矩阵的平移部分(注意,我们也可以把法向量的w分量设置为0,再乘以4×4矩阵;同样可以移除平移)。对于法向量,我们只能对它应用缩放(Scale)和旋转(Rotation)变换。 +首先,法向量只是一个方向向量,不能表达空间中的特定位置。同时,法向量没有齐次坐标(顶点位置中的w分量)。这意味着,位移不应该影响到法向量。因此,如果我们打算把法向量乘以一个模型矩阵,我们就要把模型矩阵左上角3×3的位移部分矩阵移除(译注:所谓移除就是设置为0)(注意,我们也可以把法向量的w分量设置为0,再乘以4×4矩阵;这同样可以移除位移)。对于法向量,我们只希望对它实施缩放和旋转变换。 -其次,如果模型矩阵执行了不等比缩放,法向量就不再垂直于表面了,顶点就会以这种方式被改变了。因此,我们不能用这样的模型矩阵去乘以法向量。下面的图展示了应用了不等比缩放的矩阵对法向量的影响: +其次,如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了。因此,我们不能用这样的模型矩阵来变换法向量。下面的图展示了应用了不等比缩放的模型矩阵对法向量的影响: ![](../img/02/02/basic_lighting_normal_transformation.png) -无论何时当我们提交一个不等比缩放(注意:等比缩放不会破坏法线,因为法线的方向没被改变,而法线的长度很容易通过标准化进行修复),法向量就不会再垂直于它们的表面了,这样光照会被扭曲。 +每当我们应用一个不等比缩放时(注意:等比缩放不会破坏法线,因为法线的方向没被改变,仅仅改变了法线的长度,而这很容易通过标准化来修复),法向量就不会再垂直于对应的表面了,这样光照就会被破坏。 -修复这个行为的诀窍是使用另一个为法向量专门定制的模型矩阵。这个矩阵称之为正规矩阵(Normal Matrix),它是进行了一点线性代数操作移除了对法向量的错误缩放效果。如果你想知道这个矩阵是如何计算出来的,我建议看[这个文章](http://www.lighthouse3d.com/tutorials/glsl-tutorial/the-normal-matrix/)。 +修复这个行为的诀窍是使用一个为法向量专门定制的模型矩阵。这个矩阵称之为法线矩阵(Normal Matrix),它使用了一些线性代数的操作来移除对法向量错误缩放的影响。如果你想知道这个矩阵是如何计算出来的,建议去阅读这个[文章](http://www.lighthouse3d.com/tutorials/glsl-tutorial/the-normal-matrix/)。 -正规矩阵被定义为“模型矩阵左上角的逆矩阵的转置矩阵”。真拗口,如果你不明白这是什么意思,别担心;我们还没有讨论逆矩阵(Inverse Matrix)和转置矩阵(Transpose Matrix)。注意,定义正规矩阵的大多资源就像应用到模型观察矩阵(Model-view Matrix)上的操作一样,但是由于我们只在世界空间工作(而不是在观察空间),我们只使用模型矩阵。 +法线矩阵被定义为「模型矩阵左上角的逆矩阵的转置矩阵」。真是拗口,如果你不明白这是什么意思,别担心,我们还没有讨论逆矩阵(Inverse Matrix)和转置矩阵(Transpose Matrix)。注意,大部分的资源都会将法线矩阵定义为应用到模型-观察矩阵(Model-view Matrix)上的操作,但是由于我们只在世界空间中进行操作(不是在观察空间),我们只使用模型矩阵。 -在顶点着色器中,我们可以使用`inverse`和`transpose`函数自己生成正规矩阵,`inverse`和`transpose`函数对所有类型矩阵都有效。注意,我们也要把这个被处理过的矩阵强制转换为3×3矩阵,这是为了保证它失去了平移属性,之后它才能乘以法向量。 +在顶点着色器中,我们可以使用inversetranspose函数自己生成这个法线矩阵,这两个函数对所有类型矩阵都有效。注意我们还要把被处理过的矩阵强制转换为3×3矩阵,来保证它失去了位移属性以及能够乘以`vec3`的法向量。 ```c++ -Normal = mat3(transpose(inverse(model))) * normal; +Normal = mat3(transpose(inverse(model))) * aNormal; ``` -在环境光照部分,光照表现没问题,这是因为我们没有对物体本身执行任何缩放操作,因而不是非得使用正规矩阵不可,用模型矩阵乘以法线也没错。可是,如果你进行了不等比缩放,使用正规矩阵去乘以法向量就是必不可少的了。 +在漫反射光照部分,光照表现并没有问题,这是因为我们没有对物体本身执行任何缩放操作,所以并不是必须要使用一个法线矩阵,仅仅让模型矩阵乘以法线也可以。可是,如果你进行了不等比缩放,使用法线矩阵去乘以法向量就是必不可少的了。 !!! Attention - 对于着色器来说,逆矩阵也是一种开销比较大的操作,因此,无论何时,在着色器中只要可能就应该尽量避免逆操作,因为它们必须为你场景中的每个顶点进行这样的处理。以学习的目的这样做很好,但是对于一个对于效率有要求的应用来说,在绘制之前,你最好用CPU计算出正规矩阵,然后通过uniform把值传递给着色器(和模型矩阵一样)。 + 即使是对于着色器来说,逆矩阵也是一个开销比较大的运算,因此,只要可能就应该避免在着色器中进行逆矩阵运算,它们必须为你场景中的每个顶点都进行这样的处理。用作学习目这样做是可以的,但是对于一个对效率有要求的应用来说,在绘制之前你最好用CPU计算出法线矩阵,然后通过uniform把值传递给着色器(像模型矩阵一样)。 # 镜面光照 如果你还没被这些光照计算搞得精疲力尽,我们就再把镜面高光(Specular Highlight)加进来,这样冯氏光照才算完整。 -和环境光照一样,镜面光照(Specular Lighting)同样依据光的方向向量和物体的法向量,但是这次它也会依据观察方向,例如玩家是从什么方向看着这个片段的。镜面光照根据光的反射特性。如果我们想象物体表面像一面镜子一样,那么,无论我们从哪里去看那个表面所反射的光,镜面光照都会达到最大化。你可以从下面的图片看到效果: +和漫反射光照一样,镜面光照也是依据光的方向向量和物体的法向量来决定的,但是它也依赖于观察方向,例如玩家是从什么方向看着这个片段的。镜面光照是基于光的反射特性。如果我们想象物体表面像一面镜子一样,那么,无论我们从哪里去看那个表面所反射的光,镜面光照都会达到最大化。你可以从下面的图片看到效果: ![](../img/02/02/basic_lighting_specular_theory.png) -我们通过反射法向量周围光的方向计算反射向量。然后我们计算反射向量和视线方向的角度,如果之间的角度越小,那么镜面光的作用就会越大。它的作用效果就是,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光。 +我们通过反射法向量周围光的方向来计算反射向量。然后我们计算反射向量和视线方向的角度差,如果夹角越小,那么镜面光的影响就会越大。它的作用效果就是,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光。 -观察向量是镜面光照的一个附加变量,我们可以使用观察者世界空间位置(Viewer’s World Space Position)和片段的位置来计算。之后,我们计算镜面光亮度,用它乘以光的颜色,在用它加上作为之前计算的光照颜色。 +观察向量是镜面光照附加的一个变量,我们可以使用观察者世界空间位置和片段的位置来计算它。之后,我们计算镜面光强度,用它乘以光源的颜色,再将它加上环境光和漫反射分量。 !!! Important - 我们选择在世界空间(World Space)进行光照计算,但是大多数人趋向于在观察空间(View Space)进行光照计算。在观察空间计算的好处是,观察者的位置总是(0, 0, 0),所以这样你直接就获得了观察者位置。可是,我发现出于学习的目的,在世界空间计算光照更符合直觉。如果你仍然希望在视野空间计算光照的话,那就使用观察矩阵应用到所有相关的需要变换的向量(不要忘记,也要改变正规矩阵)。 + 我们选择在世界空间进行光照计算,但是大多数人趋向于在观察空间进行光照计算。在观察空间计算的好处是,观察者的位置总是(0, 0, 0),所以这样你直接就获得了观察者位置。可是我发现在学习的时候在世界空间中计算光照更符合直觉。如果你仍然希望在观察空间计算光照的话,你需要将所有相关的向量都用观察矩阵进行变换(记得也要改变法线矩阵)。 -为了得到观察者的世界空间坐标,我们简单地使用摄像机对象的位置坐标代替(它就是观察者)。所以我们把另一个uniform添加到片段着色器,把相应的摄像机位置坐标传给片段着色器: +为了得到观察者的世界空间坐标,我们简单地使用摄像机对象的位置坐标代替(它当然就是观察者)。所以我们把另一个uniform添加到片段着色器,把相应的摄像机位置坐标传给片段着色器: ```c++ uniform vec3 viewPos; - -GLint viewPosLoc = glGetUniformLocation(lightingShader.Program, "viewPos"); -glUniform3f(viewPosLoc, camera.Position.x, camera.Position.y, camera.Position.z); ``` -现在我们已经获得所有需要的变量,可以计算高光亮度了。首先,我们定义一个镜面强度(Specular Intensity)变量`specularStrength`,给镜面高光一个中等亮度颜色,这样就不会产生过度的影响了。 - ```c++ -float specularStrength = 0.5f; +lightingShader.setVec3("viewPos", camera.Position); ``` -如果我们把它设置为1.0f,我们会得到一个对于珊瑚色立方体来说过度明亮的镜面亮度因子。下一节教程,我们会讨论所有这些光照亮度的合理设置,以及它们是如何影响物体的。下一步,我们计算视线方向坐标,和沿法线轴的对应的反射坐标: +现在我们已经获得所有需要的变量,可以计算高光强度了。首先,我们定义一个镜面强度(Specular Intensity)变量,给镜面高光一个中等亮度颜色,让它不要产生过度的影响。 + +```c++ +float specularStrength = 0.5; +``` + +如果我们把它设置为1.0f,我们会得到一个非常亮的镜面光分量,这对于一个珊瑚色的立方体来说有点太多了。下一节教程中我们会讨论如何合理设置这些光照强度,以及它们是如何影响物体的。下一步,我们计算视线方向向量,和对应的沿着法线轴的反射向量: ``` vec3 viewDir = normalize(viewPos - FragPos); vec3 reflectDir = reflect(-lightDir, norm); ``` -需要注意的是我们使用了`lightDir`向量的相反数。`reflect`函数要求的第一个是从光源指向片段位置的向量,但是`lightDir`当前是从片段指向光源的向量(由先前我们计算`lightDir`向量时,(减数和被减数)减法的顺序决定)。为了保证我们得到正确的`reflect`坐标,我们通过`lightDir`向量的相反数获得它的方向的反向。第二个参数要求是一个法向量,所以我们提供的是已标准化的`norm`向量。 +需要注意的是我们对`lightDir`向量进行了取反。`reflect`函数要求第一个向量是**从**光源指向片段位置的向量,但是`lightDir`当前正好相反,是从片段**指向**光源(由先前我们计算`lightDir`向量时,减法的顺序决定)。为了保证我们得到正确的`reflect`向量,我们通过对`lightDir`向量取反来获得相反的方向。第二个参数要求是一个法向量,所以我们提供的是已标准化的`norm`向量。 -剩下要做的是计算镜面亮度分量。下面的代码完成了这件事: +剩下要做的是计算镜面分量。下面的代码完成了这件事: ```c++ float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32); vec3 specular = specularStrength * spec * lightColor; ``` -我们先计算视线方向与反射方向的点乘(确保它不是负值),然后得到它的32次幂。这个32是高光的**发光值(Shininess)**。一个物体的发光值越高,反射光的能力越强,散射得越少,高光点越小。在下面的图片里,你会看到不同发光值对视觉(效果)的影响: +我们先计算视线方向与反射方向的点乘(并确保它不是负值),然后取它的32次幂。这个32是高光的反光度(Shininess)。一个物体的反光度越高,反射光的能力越强,散射得越少,高光就会越小。在下面的图片里,你会看到不同反光度的视觉效果影响: ![](../img/02/02/basic_lighting_specular_shininess.png) -我们不希望镜面成分过于显眼,所以我们把指数设置为32。剩下的最后一件事情是把它添加到环境光颜色和散射光颜色里,然后再乘以物体颜色: +我们不希望镜面成分过于显眼,所以我们把指数保持为32。剩下的最后一件事情是把它加到环境光分量和漫反射分量里,再用结果乘以物体的颜色: ```c++ vec3 result = (ambient + diffuse + specular) * objectColor; -color = vec4(result, 1.0f); +FragColor = vec4(result, 1.0); ``` -我们现在为冯氏光照计算了全部的光照元素。根据你的观察点,你可以看到类似下面的画面: +我们现在为冯氏光照计算了全部的光照分量。根据你的视角,你可以看到类似下面的画面: ![](../img/02/02/basic_lighting_specular.png) -你可以[在这里找到完整源码](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting_specular),在这里有[顶点](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting&type=fragment)着色器。 +你可以在[这里](https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/2.2.basic_lighting_specular/basic_lighting_specular.cpp)找到完整源码。 !!! Important - 早期的光照着色器,开发者在顶点着色器中实现冯氏光照。在顶点着色器中做这件事的优势是,相比片段来说,顶点要少得多,因此会更高效,所以(开销大的)光照计算频率会更低。然而,顶点着色器中的颜色值是只是顶点的颜色值,片段的颜色值是它与周围的颜色值的插值。结果就是这种光照看起来不会非常真实,除非使用了大量顶点。 + 在光照着色器的早期,开发者曾经在顶点着色器中实现冯氏光照模型。在顶点着色器中做光照的优势是,相比片段来说,顶点要少得多,因此会更高效,所以(开销大的)光照计算频率会更低。然而,顶点着色器中的最终颜色值是仅仅只是那个顶点的颜色值,片段的颜色值是由插值光照颜色所得来的。结果就是这种光照看起来不会非常真实,除非使用了大量顶点。 ![](../img/02/02/basic_lighting_gouruad.png) - 在顶点着色器中实现的冯氏光照模型叫做Gouraud着色,而不是冯氏着色。记住由于插值,这种光照连起来有点逊色。冯氏着色能产生更平滑的光照效果。 + 在顶点着色器中实现的冯氏光照模型叫做Gouraud着色(Gouraud Shading),而不是冯氏着色(Phong Shading)。记住,由于插值,这种光照看起来有点逊色。冯氏着色能产生更平滑的光照效果。 -现在你可以看到着色器的强大之处了。只用很少的信息,着色器就能计算出光照,影响到为我们所有物体的片段颜色。[下一节](03 Materials.md)中,我们会更深入的研究光照模型,看看我们还能做些什么。 +现在你应该能够看到着色器的强大之处了。只用很少的信息,着色器就能计算出光照如何影响到所有物体的片段颜色。[下一节](03 Materials.md)中,我们会更深入的研究光照模型,看看我们还能做些什么。 ## 练习 -- 目前,我们的光源时静止的,你可以尝试使用`sin`和`cos`函数让光源在场景中来回移动,此时再观察光照效果能让你更容易理解冯氏光照模型。[参考解答](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting-exercise1)。 -- 尝试使用不同的环境光、散射镜面强度,观察光照效果。改变镜面光照的`shininess`因子试试。 -- 在观察空间(而不是世界空间)中计算冯氏光照:[参考解答](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting-exercise2)。 -- 尝试实现一个Gouraud光照来模拟冯氏光照,[参考解答](http://learnopengl.com/code_viewer.php?code=lighting/basic_lighting-exercise3)。 +- 目前,我们的光源时静止的,你可以尝试使用sincos函数让光源在场景中来回移动。观察光照随时间的改变能让你更容易理解冯氏光照模型。[参考解答](https://learnopengl.com/code_viewer.php?code=lighting/basic_lighting-exercise1)。 +- 尝试使用不同的环境光、漫反射和镜面强度,观察它们怎么是影响光照效果的。同样,尝试实验一下镜面光照的反光度因子。尝试理解为什么某一个值能够有着某一种视觉输出。 +- 在观察空间(而不是世界空间)中计算冯氏光照:[参考解答](https://learnopengl.com/code_viewer.php?code=lighting/basic_lighting-exercise2)。 +- 尝试实现一个Gouraud着色(而不是冯氏着色)。如果你做对了话,立方体的光照应该会[看起来有些奇怪](../img/02/02/basic_lighting_exercise3.png),尝试推理为什么它会看起来这么奇怪:[参考解答](https://learnopengl.com/code_viewer.php?code=lighting/basic_lighting-exercise3)。 diff --git a/docs/img/02/02/basic_lighting_exercise3.png b/docs/img/02/02/basic_lighting_exercise3.png new file mode 100644 index 0000000000000000000000000000000000000000..e6e3bb5a1cbc7c4a9b1ca95e375ba820a9048827 GIT binary patch literal 20727 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 vPmWfC!?ulLnZy}E0l<>hg4J-!{=NX