From 0c90adeb620d52fd02f986b4ddd757f1509b34cc Mon Sep 17 00:00:00 2001 From: GingerStar Date: Sun, 21 Jun 2015 16:38:34 +0800 Subject: [PATCH] commit new chapters --- 02 Lighting/03 Materials.md | 143 +++++++++ 02 Lighting/04 Lighting maps.md | 127 ++++++++ 02 Lighting/05 Light casters.md | 497 ++++++++++++++++++++++++++++++++ 3 files changed, 767 insertions(+) create mode 100644 02 Lighting/03 Materials.md create mode 100644 02 Lighting/04 Lighting maps.md create mode 100644 02 Lighting/05 Light casters.md diff --git a/02 Lighting/03 Materials.md b/02 Lighting/03 Materials.md new file mode 100644 index 0000000..ce69518 --- /dev/null +++ b/02 Lighting/03 Materials.md @@ -0,0 +1,143 @@ +本文作者JoeyDeVries,由[Django(http://bullteacher.com/14-materials.html)翻译自[http://learnopengl.com](http://learnopengl.com/#!Lighting/Materials) + +在真实世界里,每个物体会对光产生不同的反作用。钢比陶瓷花瓶更闪闪发光。比如一块木头不会和钢一样对光做出相同反作用。每个物体对specular高光也有不同的反应。有些物体不会散射很多光却会反射很多光,结果看起来就有一个较小的高光点,其他物体散射的更多,就会产生一个半径更大的高光。如果我们想要在OpenGL中模拟几种类型的物体,我们必须为每个物体定义材质属性。 + +在前面的教程中,我们指定一个物体和一个光的颜色来定义物体的图像输出,并使之结合ambient和specular亮度元素。当描述物体的时候,我们可以为3种光照元素:ambient、diffuse、specular光照分别定义一个材质颜色。通过为每个元素指定一个颜色,我们已经对物体的颜色输出有了精密的控制。现在把一个发亮元素添加到这三个颜色里,这是我们需要的所有材质属性: + +```c++ +#version 330 core +struct Material +{ + vec3 ambient; + vec3 diffuse; + vec3 specular; + float shininess; +}; +uniform Material material; +``` +在像素着色器中,我们创建一个结构体,来储存物体的材质属性。我们也可以把它们储存为独立的uniform值,但是作为一个结构体来储存可以更有条理。我们首先定义结构体的布局,然后简单声明一个uniform变量,以新近创建的结构体作为它的类型。 + +就像你所看到的,我们为每个Phong光照的元素都定义一个颜色向量。ambient材质向量定义了在ambient光照下这个物体反射的是什么颜色;通常这是和物体颜色相同的颜色。diffuse材质向量定义了在diffuse光照下物体的颜色。diffuse颜色被设置为(和ambient光照一样)物体要求的颜色。specular材质向量设置的是物体受到的specular光的影响的颜色(或者可能是反射一个物体特定的specular高光颜色)。最后,shininess影响specular高光的散射/半径。 + +这四个元素定义了一个物体的材质,通过它们我们能够模拟很多真实世界的材质。这里有一个列表devernay.free.fr展示了几种材质属性,这些材质属性模拟外部世界的真实材质。下面的图片展示了几种真实世界材质对我们的立方体的影响: +![](http://www.learnopengl.com/img/lighting/materials_real_world.png) +就像你所看到的,正确的指定一个物体的材质属性,似乎就是改变我们物体的相关属性的比例。效果显然很引人注目,但是对于大多数真实效果,我们最终需要更加复杂的形状,而不单单是一个立方体。在下面教程部分里,我们会讨论更复杂的形状。 + +为一个物体赋予正确的材质是一件高难的手艺,这需要大量实验和丰富的经验,所以由于错误的设置材质而毁了物体的画面质量是件经常发生的事。 + +让我们试试在着色器中实现这样的一个材质系统。 + + +##14.1 设置材质 + +我们在像素着色器中创建了一个uniform材质结构体,所以下面我们希望改变光照计算来顺应新的材质属性。由于所有材质变量都是储存在结构体中,我们可以从uniform变量material取得它们: +```c++ +void main() +{ + // Ambient + vec3 ambient = lightColor * material.ambient; + + // Diffuse + vec3 norm = normalize(Normal); + vec3 lightDir = normalize(lightPos - FragPos); + float diff = max(dot(norm, lightDir), 0.0); + vec3 diffuse = lightColor * (diff * material.diffuse); + + // Specular + vec3 viewDir = normalize(viewPos - FragPos); + vec3 reflectDir = reflect(-lightDir, norm); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); + vec3 specular = lightColor * (spec * material.specular); + + vec3 result = ambient + diffuse + specular; + color = vec4(result, 1.0f); +} +``` +就像你所看到的,我么现在获得所有材质结构体的属性,无论在哪儿我们都需要它们,这次通过材质颜色的帮助,计算结果输出的颜色。物体的每个材质属性都乘以它们对应的光照元素。 + +通过设置适当的uniform,我们可以在应用中设置物体的材质。当设置uniform时,在GLSL中的一个结构体不会被认为有什么特别之处。一个结构体值扮演uniform变量的封装体,所以如果我们希望填充这个结构体,我们就仍然必须设置单独的uniform,但是这次带有结构体名字作为前缀: +```c++ +GLint matAmbientLoc = glGetUniformLocation(lightingShader.Program, "material.ambient"); +GLint matDiffuseLoc = glGetUniformLocation(lightingShader.Program, "material.diffuse"); +GLint matSpecularLoc = glGetUniformLocation(lightingShader.Program, "material.specular"); +GLint matShineLoc = glGetUniformLocation(lightingShader.Program, "material.shininess"); + +glUniform3f(matAmbientLoc, 1.0f, 0.5f, 0.31f); +glUniform3f(matDiffuseLoc, 1.0f, 0.5f, 0.31f); +glUniform3f(matSpecularLoc, 0.5f, 0.5f, 0.5f); +glUniform1f(matShineLoc, 32.0f); +``` +我们设置ambient和diffuse元素我们喜欢物体所呈现的颜色,设置物体的specular元素为中等亮度颜色;我们不希望specular元素对这个指定物体产生过于强烈的影响。我们同样保持光亮为32。我们现在可以简单的在应用中影响物体的材质。 + +运行程序,会得到下面这样的结果: +![](http://www.learnopengl.com/img/lighting/materials_with_material.png) +看起来真不怎么样对吗? + + +##14.2 光的属性 + +这个物体太亮了。物体过亮的原因是ambient、diffuse和specular颜色被从任何一个光源全力地反射。光源对ambient、diffuse和specular元素同时有着不同的亮度。前面的教程,我们通过使用一个强度值改变ambient和specular亮度的方式,解决了这个问题,但是这次为每个光照元素指定了亮度向量。如果我们想象lightColor是vec3(1.0),代码看起来像是这样: +```c++ +vec3 ambient = vec3(1.0f) * material.ambient; +vec3 diffuse = vec3(1.0f) * (diff * material.diffuse); +vec3 specular = vec3(1.0f) * (spec * material.specular); +``` +所以物体的每个材质属性返回了每个光照元素的全亮度。这些vec3(1.0)值可以各自独立的影响每个光源,这通常就是我们想要的。现在物体的ambient元素完全地影响了立方体的颜色,可视ambient元素uyinggai对最终颜色有这么大的影响,所以我们要通过设置光的ambient亮度为一个小一点的值的方式,限制ambient颜色: +```c++ +vec3 result = vec3(0.1f) * material.ambient; +``` +我们可以用同样的方式影响光源的diffuse和specular亮度。这和我们前面教程所做的极为相似;你可以说我们已经创建了一些光的属性各自独立地来影响每个光照元素。我们希望为光的属性创建一些与材质结构体相似的东西: +```c++ +struct Light +{ + vec3 position; + vec3 ambient; + vec3 diffuse; + vec3 specular; +}; +uniform Light light; +``` +一个光源的ambient、diffuse和specular光都有不同的亮度。ambient光通常设置为一个比较低的亮度,因为饿哦们不希望ambient颜色成为主导。光源的diffuse元素通常设置为我们希望光所具有的颜色;经常是一个明亮的白色。specular元素通常设置为vec3(1.0f)的全亮度的发光度。要记住的是我们同样把光的位置添加到结构体中。 + +就像材质uniform一样,我么你需要更新像素着色器: +```c++ +vec3 ambient = light.ambient * material.ambient; +vec3 diffuse = light.diffuse * (diff * material.diffuse); +vec3 specular = light.specular * (spec * material.specular); +``` +然后我们要在应用里设置光的亮度: +```c++ +GLint lightAmbientLoc = glGetUniformLocation(lightingShader.Program, "light.ambient"); +GLint lightDiffuseLoc = glGetUniformLocation(lightingShader.Program, "light.diffuse"); +GLint lightSpecularLoc = glGetUniformLocation(lightingShader.Program, "light.specular"); + +glUniform3f(lightAmbientLoc, 0.2f, 0.2f, 0.2f); +glUniform3f(lightDiffuseLoc, 0.5f, 0.5f, 0.5f); // Let's darken the light a bit to fit the scene +glUniform3f(lightSpecularLoc, 1.0f, 1.0f, 1.0f); +``` +现在,我们调整了光如何影响物体所有的材质的,我们得到一个更像前面教程的视觉输出。这次我们完全控制了物体光照和材质: +![](http://www.learnopengl.com/img/lighting/materials_light.png) +现在改变物体的外观相对简单了些。我们做点更有趣的事! + + + +##14.3 不同的光的颜色 + +目前为止,我们只是使用光的颜色去改变它们的独立元素,选择的是从白到灰到黑范围的颜色,不是动感的物体的真实颜色(只是亮度)。由于我们现在简单的取得了光的属性,我们可以随着时间改变它们的颜色,获得的效果有很意思。由于没见识都已经在像素着色器做好了,改变光的颜色很简单,可以立即创建出一些有趣的效果: + +就像你所看见的,不同的光的颜色极大地影响了物体的颜色输出。由于光的颜色直接影响物体反射的颜色(你可能想起颜色教程),它对视觉输出有显著的影响。 + +利用sin和glfwGetTime,改变光的ambient和diffuse颜色,我们可以随着时间流逝简单的改变光的颜色: +```c++ +glm::vec3 lightColor; lightColor.x = sin(glfwGetTime() * 2.0f); +lightColor.y = sin(glfwGetTime() * 0.7f); +lightColor.z = sin(glfwGetTime() * 1.3f); + +glm::vec3 diffuseColor = lightColor * glm::vec3(0.5f); // Decrease the influence +glm::vec3 ambientColor = diffuseColor * glm::vec3(0.2f); // Low influence + +glUniform3f(lightAmbientLoc, ambientColor.x, ambientColor.y, ambientColor.z); +glUniform3f(lightDiffuseLoc, diffuseColor.x, diffuseColor.y, diffuseColor.z); +``` +尝试和实验使用这些光照和材质值,看看它们怎样影响图像输出的。你可以从这里找到应用,和像素着色器的源码。 diff --git a/02 Lighting/04 Lighting maps.md b/02 Lighting/04 Lighting maps.md new file mode 100644 index 0000000..c7aca8d --- /dev/null +++ b/02 Lighting/04 Lighting maps.md @@ -0,0 +1,127 @@ +本文作者JoeyDeVries,由[Django(http://bullteacher.com/15-lighting-maps.html)翻译自[http://learnopengl.com](http://www.learnopengl.com/#!Lighting/Lighting-maps) + +前面的教程,我们讨论了每个物体都拥有各自不同的材质,因而会对光做出不同的反应。在一个光照场景中,给每个物体和其他物体不同的外观很棒,但是这仍然不会对一个物体的图像输出提供很多的伸缩性。 + +前面的教程我们为一个物体的作为一个整体定义了一个材质,但是现实世界的物体通常不会只有这么一种材质,而是由多种材质组成。想象一辆车:它的外表质地光亮,车窗会部分反射环境,它的轮胎没有specular高光,轮彀却非常闪亮(在洗过之后)。汽车同样有diffuse和ambient颜色,它们在整个车上都不相同;一辆车显示了多种不同的ambient/diffuse颜色。总之,这样一个物体每个部分都有多种材质属性。 + +所以,前面的材质系统对于除了最简单的模型以外都是不够的,所以我们需要扩展前面的系统,我们要介绍diffuse和specular贴图。它们允许你对一个物体的diffuse(对于简洁的ambient成分来说,它们几乎总是是一样的)和specular成分能够有更精确的影响。 + + + +##15.1 diffuse贴图 + +我们希望通过某种方式对每个原始像素独立设置diffuse颜色。有可以让我们基于物体原始像素的位置来获取颜色值的系统吗? + +这可能听起来极其相似,坦白来讲我们我们使用这样的系统已经有一会儿了。听起来很像在一个较早的教程中谈论的纹理,它基本就是一个纹理。我们其实是使用同一个潜在原则下的不同名称:使用一张图片包裹住物体,我们为每个原始像素索引独立颜色值。在有光的场景里,通常叫做diffuse贴图(这通常是3D艺术家的叫法),因为这个纹理图像表现了所有物体的diffuse颜色。 + +为了强调diffuse贴图,我们将会使用下面的图片,它是一个有一圈钢边的木箱: + +![](http://www.learnopengl.com/img/textures/container2.png) + +在着色器中使用diffuse贴图和纹理教程介绍的一样。这次我们把纹理储存为sampler2D,它在Material结构体中。我们使用diffuse贴图替代早期定义的vec3类型的diffuse颜色。 + +要记住的是sampler2D也叫做模糊类型,这意味着我们不能以某种类型对它实例化,只能用uniform定义它们。如果我们用结构体而不是uniform实例化(就像函数的参数那样),GLSL会抛出奇怪的错误;这同样也适用于其他模糊类型。 +我们也要移除amibient材质颜色向量,因为ambient颜色绝大多数情况等于diffuse颜色,所以不需要分别去储存它: +```c++ +struct Material +{ + sampler2D diffuse; + vec3 specular; + float shininess; +}; +... +in vec2 TexCoords; +``` +如果你非把ambient颜色设置为不同的值不可(不同于diffuse值),你可以继续保持ambient的vec3,但是整个物体的ambient颜色会继续保持不变。为了使每个原始像素得到不同ambient值,你需要对ambient值单独使用另一个纹理。 +注意,在像素着色器中我们将会再次需要纹理坐标,所以我们声明一个额外输入变量。然后我们简单地从纹理采样,来获得原始像素的diffuse颜色值: +```c++ +vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords)); +``` +同样,不要忘记把ambient材质的颜色设置为diffuse材质的颜色: +```c++ +vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords)); +``` +这就是diffuse贴图的全部内容了。就像你看到的,这不是什么新的东西,但是它却极大提升了视觉品质。为了让它工作,我们需要用到纹理坐标更新顶点数据,把它们作为顶点属性传递到像素着色器,把纹理加载,把纹理绑定到合适的纹理单元。 + +更新的顶点数据可以从这里找到。顶点数据现在包括了顶点位置,法线向量和纹理坐标,每个立方体的顶点都有这些属性。让我们更新顶点着色器来接受纹理坐标作为顶点属性,然后发送到像素着色器: +```c++ +#version 330 core +layout (location = 0) in vec3 position; +layout (location = 1) in vec3 normal; +layout (location = 2) in vec2 texCoords; +... +out vec2 TexCoords; + +void main() +{ + ... + TexCoords = texCoords; +} +``` +要保证更新的顶点属性指针,不仅是VAO匹配新的顶点数据,也要把箱子图片加载为纹理。在绘制箱子之前,我们希望首选纹理单元被赋为material.diffuse这个uniform采样器,并绑定箱子的纹理到这个纹理单元: +```c++ +glUniform1i(glGetUniformLocation(lightingShader.Program, "material.diffuse"), 0); +... +glActiveTexture(GL_TEXTURE0); +glBindTexture(GL_TEXTURE_2D, diffuseMap); +``` +现在,使用一个diffuse贴图,我们在细节上再次获得惊人的提升,这次添加到箱子上的光照开始闪光了(名符其实)。你的箱子现在可能看起来像这样: + +![](http://www.learnopengl.com/img/lighting/materials_diffuse_map.png) + +你可以在这里得到应用的全部代码。 + + + +##15.2 specular贴图 + +你可能注意到,specular高光看起来不怎么样,由于我们的物体是个箱子,大部分是木头,我们知道木头是不会有镜面高光的。我们通过把物体设置specular材质设置为vec3(0.0f)来修正它。但是这样意味着铁边会不再显示镜面高光,我们知道钢铁是会显示一些镜面高光的。我们会想要控制物体部分地显示镜面高光,它带有修改了的亮度。这个问题看起来和diffuse贴图的讨论一样。是巧合吗?我想不是。 + +我们同样适用一个纹理贴图,来获得镜面高光。这意味着我们需要生成一个黑白(或者你喜欢的颜色)纹理来定义specular亮度,把它应用到物体的每个部分。下面是一个specular贴图的例子: + +![](http://www.learnopengl.com/img/textures/container2_specular.png) + +一个specular高光的亮度可以通过图片中每个纹理的亮度来获得。specular贴图的每个像素可以显示为一个颜色向量,比如:在那里黑色代表颜色向量vec3(0.0f),灰色是vec3(0.5f)。在像素着色器中,我们采样相应的颜色值,把它乘以光的specular亮度。像素越“白”,乘积的结果越大,物体的specualr部分越亮。 + +由于箱子几乎是由木头组成,木头作为一个材质不会有镜面高光,整个不透部分的diffuse纹理被用黑色覆盖:黑色部分不会包含任何specular高光。箱子的铁边有一个修改的specular亮度,它自身更容易受到镜面高光影响,木纹部分则不会。 + +从技术上来讲,木头也有镜面高光,尽管这个闪亮值很小(更多的光被散射),影响很小,但是为了学习目的,我们可以假装木头不会有任何specular光反射。 +使用Photoshop或Gimp之类的工具,剪切一些部分,非常容易变换一个diffuse纹理,为specular图片,以增加亮度/对比度的方式,可以把这个部分变换为黑色或白色。 + + + +###15.2.1 specular贴图采样 + +一个specular贴图和其他纹理一样,所以代码和diffuse贴图的代码也相似。确保合理的加载了图片,生成一个纹理对象。由于我们在同样的像素着色器中使用另一个纹理采样器,我们必须为specular贴图使用一个不同的纹理单元(查看纹理),所以在渲染前让我们把它绑定到合适的纹理单元 +```c++ +glUniform1i(glGetUniformLocation(lightingShader.Program, "material.specular"), 1); +... +glActiveTexture(GL_TEXTURE1); +glBindTexture(GL_TEXTURE_2D, specularMap); +``` +然后更新像素着色器材质属性,接受一个sampler2D作为这个specular部分的类型,而不是vec3: +```c++ +struct Material +{ + sampler2D diffuse; + sampler2D specular; + float shininess; +}; +``` +最后我们希望采样这个specular贴图,来获取原始像素相应的specular亮度: +```c++ +vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords)); +vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords)); +vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords)); +color = vec4(ambient + diffuse + specular, 1.0f); +``` +通过使用一个specular贴图我们可以定义极为精细的细节,物体的这个部分会获得闪亮的属性,我们可以设置它们相应的亮度。specular贴图给我们一个附加的高于diffuse贴图的控制权限。 + +如果你不想成为主流,你可以在specular贴图里使用颜色,不单单为每个原始像素设置specular亮度,同时也设置specular高光的颜色。从真实角度来说,specular的颜色基本是由光源自身决定的,所以它不会生成真实的图像(这就是为什么图片通常是黑色和白色的:我们只关心亮度)。 +如果你现在运行应用,你可以清晰地看到箱子的材质现在非常类似真实的铁边的木头箱子了: + +![](http://www.learnopengl.com/img/lighting/materials_specular_map.png) + +你可以在这里找到全部源码。也对比一下你的顶点着色器和像素着色器。 + +使用diffuse和specular贴图,我们可以给相关但简单物体添加一个极为明显的细节。我们可以使用其他纹理贴图,比如法线/bump贴图或者反射贴图,给物体添加更多的细节。但是这些在后面教程才会涉及。把你的箱子给你所有的朋友和家人看,有一天你会很满足,我们的箱子会比现在更漂亮! \ No newline at end of file diff --git a/02 Lighting/05 Light casters.md b/02 Lighting/05 Light casters.md new file mode 100644 index 0000000..5272f48 --- /dev/null +++ b/02 Lighting/05 Light casters.md @@ -0,0 +1,497 @@ +本文作者JoeyDeVries,由[Django(http://bullteacher.com/16-light-casters.html)翻译自[http://learnopengl.com](http://www.learnopengl.com/#!Lighting/Light-casters) + +我们目前使用的所有光照都来自于一个单独的光源,这是空间中的一个点。它的效果不错,但是在真实世界,我们有多种类型的光,它们每个表现都不同。一个光源把光投射到物体上,叫做投光。这个教程里我们讨论几种不同的投光类型。学习模拟不同的光源是你未来丰富你的场景的另一个工具。 + +我们首先讨论定向光(directional light),接着是作为之前学到知识的扩展的点光(point light),最后我们讨论聚光灯(spot light)。下面的教程我们会把这几种不同的光类型整合到一个场景中。 + + + +##16.1 定向光 + +当一个光源很远的时候,来自光源的每条光线接近于平行。这看起来就像所有的光线来自于同一个方向,无论物体和观察者在哪儿。当一个光源被设置为无限远时,它被称为定向光,因为所有的光线都有着同一个方向;它会独立于光源的位置。 + +我们知道的定向光源的一个好例子是,太阳。太阳和我们不是无限远,但它太远了,在计算光照的时候,我们感觉它就像无限远。在下面的图片里,来自于太阳的所有的光线都被定义为平行光: + +![](http://www.learnopengl.com/img/lighting/light_casters_directional.png) + +因为所有的光线都是平行的,对于场景中的每个物体光的方向都保持一致,物体和光源的位置保持怎样的关系都无所谓。由于光的方向向量保持一致,光照计算会和场景中的其他物体相似。 + +我们可以通过定义一个光的方向向量,来模拟这样一个定向光,而不是使用光的位置向量。着色器计算保持大致相同的要求,这次我们直接使用光的方向向量来代替用lightDir向量和position向量的计算: +```c++ +struct Light +{ + // vec3 position; // No longer necessery when using + directional lights. + vec3 direction; + vec3 ambient; + vec3 diffuse; + vec3 specular; +}; +... +void main() +{ + vec3 lightDir = normalize(-light.direction); + ... +} +``` +注意,我们首先忽略light.direction向量。目前我们使用的光照计算需要光的方向作为一个来自片段朝向的光源的方向,但是人们通常更习惯定义一个定向光作为一个全局方向,它从光源发出。所以我们必须忽略全局光的方向向量来改变它的方向;它现在是一个方向向量指向光源。同时,确保对向量进行归一处理,因为假定输入的向量就是一个单位向量是不明智的。 + +作为结果的lightDir向量被使用在diffuse和specular计算之前。 + +为了清晰地强调一个定向光对所有物体都有同样的影响,我们再次访问坐标系教程结尾部分的箱子场景。例子里我们先定义10个不同的箱子位置,为每个箱子生成不同的模型矩阵,每个模型矩阵包含相应的本地到世界变换: +```c++ +for(GLuint i = 0; i < 10; i++) +{ + model = glm::mat4(); + 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)); + glDrawArrays(GL_TRIANGLES, 0, 36); +} +``` +同时,不要忘记定义光源的方向(注意,我们把方向定义为:从光源处发出的方向;在下面,你可以快速看到光的方向的指向): +```c++ +GLint lightDirPos = glGetUniformLocation(lightingShader.Program, "light.direction"); +glUniform3f(lightDirPos, -0.2f, -1.0f, -0.3f); +``` +我们已经把光的位置和方向向量传递为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,我们有一个光的方向向量,所以根据那个调整计算方法: +```c++ +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 (like last tutorial) + ``` +有趣的事实:这就是旧OpenGL(固定函数式)决定一个光源是一个定向光还是位置光源,更具这个修改它的光照。 +如果你现在编译应用,飞跃场景,它看起来像有一个太阳一样的光源,把光抛到物体身上。你可以看到diffuse和specular元素都反射了,就像天空上有一个光源吗?看起来就像这样: + +![](http://www.learnopengl.com/img/lighting/light_casters_directional_light.png) + +你可以在这里获得应用的所有代码,这里是顶点和像素着色器代码。 + + + +##16.2 点光 + +定向光作为全局光可以照亮整个场景,这非常棒,但是另一方面除了定向光,我们通常也需要几个点光,在场景里发亮。点光是一个在时间里有位置的光源,它向所有方向发光,光线随距离增加逐渐变暗。想象灯泡和火炬作为投光物,它们可以扮演点光的角色。 + +![](http://www.learnopengl.com/img/lighting/light_casters_point.png) + +之前的教程我们已经使用了(最简单的)点光。我们有一个有位置的光源,它从自身的位置向所有方向发出光线。然而,这个我们定义的光源所模拟光线的从不会衰减,这使得它看起来光源亮度极强。在大多数3D模拟中,我们喜欢模拟一个之恩那个照亮一个周围确定区域光源,它不会照亮整个场景。 + +如果你把10个箱子添加到之前教程的光照场景中,你会注意到黑暗中的每个箱子都会有同样的亮度,就像箱子在光照的前面;没有公式定义光的距离衰减。我们想让黑暗中与光源比较近的箱子被轻微地照亮。 + + + +##16.3 衰减 + +随着光线穿越更远的距离相应地减少亮度,通常被称为衰减。一种随着距离减少亮度的方式是使用线性等式。这样的一个随着距离减少亮度的线性方程,可以使远处的物体更暗。然而,这样的线性方程效果会有点假。在真实世界,通常光在近处时非常亮,但是一个光源的亮度,开始的时候减少的非常快,之后随着距离的增加,减少的速度会慢下来。我们需要一种不同的方程来减少光的亮度。 + +幸运的是一些聪明人已经早就把它想到了。下面的方程把一个片段的光的亮度除以一个已经计算出来的衰减值,这个值根据光源的远近得到: + + + + + (1) + + + + F + + a + t + t + + + = + + I + + + K + c + + + + + K + l + + + d + + + + K + q + + + + d + 2 + + + + + + + +在这里I是当前片段的光的亮度,d代表片段到光源的距离。为了计算衰减值,我们定义3个项:常数项Kc,一次项Kl和二次项Kq。 + +常数项通常是1.0,它的作用是保证坟墓永远不会比1小,因为它可以利用一定的距离增加亮度,这个结果不会影响到我们所寻找的。 +一次项用于与距离值相称,这回以线性的方式减少亮度。 +二次项用于与距离的平方相乘,为光源设置一个亮度的二次递减。二次项在距离比较近的时候相比一次项会比一次项更小,但是当距离更远的时候比一次项更大。 +由于二次项的光会以线性方式减少,指导距离足够大的时候,就会超过一次项,之后,光的亮度会减少的更快。最后的效果就是光在近距离时,非常量,但是距离变远亮度迅速降低,最后亮度降低速度再次变慢。下面的图展示了在100以内的范围,这样的衰减效果。 + +![](http://www.learnopengl.com/img/lighting/attenuation.png) + +你可以看到当距离很近的时候光有最强的亮度,但是随着距离增大,亮度明显减弱,大约接近100的时候,就会慢下来。这就是我们想要的。 + + + +###16.3.1 选择正确的值 + +但是,我们把这三个项设置为什么值呢?正确的值的设置由很多因素决定:环境、你希望光所覆盖的距离范围、光的类型等。大多数场合,这是经验的问题,也要适度调整。下面的表格展示一些各项的值,它们模拟现实(某种类型的)光源,覆盖特定的半径(距离)。第一蓝定义一个光的距离,它覆盖所给定的项。这些值是大多数光的良好开始,它是来自Ogre3D的维基的礼物: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
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
+就像你所看到的,常数项Kc一直都是1.0。一次项Kl为了覆盖更远的距离通常很小,二次项Kq就更小了。尝试用这些值进行实验,看看它们在你的实现中各自的效果。我们的环境中,32到100的距离对大多数光通常就足够了。 + + + +###16.3.2 实现衰减 + +为了实现衰减,在着色器中我们会需要三个额外数值:也就是公式的常量、一次项和二次项。最好把它们储存在之前定义的Light结构体中。要注意的是我们计算lightDir,就是在前面的教程中我们所做的,不是像之前的定向光的那部分。 +```c++ +struct Light +{ + vec3 position; + vec3 ambient; + vec3 diffuse; + vec3 specular; + float constant; + float linear; + float quadratic; +}; +``` +然后,我们在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); + +``` +在像素着色器中实现衰减很直接:我们根据公式简单的计算衰减值,在乘以ambient、diffuse和specular元素。 + +我们需要光源的距离提供给公式;记得我们能够计算向量的长度吗?我们可以通过获取片段和光源之间的不同向量把向量的长度结果作为距离项。我们可以使用GLSL的内建length函数做这件事: +```c++ +float distance = length(light.position - Position); +float attenuation = 1.0f / (light.constant + light.linear*distance +light.quadratic*(distance*distance)); +``` +然后,我们在光照计算中,通过把衰减值乘以ambient、diffuse和specular颜色,包含这个衰减值。 + +我们可以可以把ambient元素留着不变,这样amient光照就不会随着距离减少,但是如果我们使用多余1个的光源,所有的ambient元素会开始叠加,因此这种情况,我们希望ambient光照也衰减。简单的调试出对于你的环境来说最好的效果。 +```c++ +ambient *= attenuation; +diffuse *= attenuation; +specular *= attenuation; +``` +如果你运行应用后获得这样的效果: + +light_casters_point_light + +你可以看到现在只有最近处的箱子的前面被照得最亮。后面的箱子一点都没被照亮,因为它们距离光源太远了。你可以在这里找到应用源码和片段的代码。 + +电光就是一个可配的置位置和衰减值应用到光照计算中。还有另一种类型光可用于我们照明库当中。 + + + +##16.4 聚光灯 + +我们要讨论的最后一种类型光是聚光灯。聚光灯是一种位于环境中某处的光源,它不是向所有方向照射,而是只朝某个方向照射。结果是只有一个聚光灯照射方向的确定半径内的物体才会被照亮,其他的都保持黑暗。聚光灯的好例子是路灯或手电筒。 + +OpenGL中的聚光灯用世界空间位置,一个方向和一个指定了聚光灯半径的切光角来表示。我们计算的每个片段,如果片段在聚光灯的切光方向之间(就是在圆锥体内),我们就会把片段照亮。下面的图可以让你明白聚光灯是如何工作的: + +![](http://www.learnopengl.com/img/lighting/light_casters_spotlight_angles.png) + +* LightDir:从片段指向光源的向量。 +* SpotDir:聚光灯所指向的方向。 +* Phiφ:定义聚光灯半径的切光角。每个落在这个角度之外的,聚光灯都不会照亮。 +* Thetaθ:LightDir向量和SpotDir向量之间的角度。θ值应该比φ值小,这样才会在聚光灯内。 + +所以我们大致要做的是,计算LightDir向量和SpotDir向量的点乘(返回两个单位向量的点乘,还记得吗?),然后在和遮光角φ对比。现在你应该明白聚光灯是我们下面将创建的手电筒的范例。 + + + +##16.5 手电筒 + +手电筒是一个坐落在观察者位置的聚光灯,通常瞄准玩家透视图的前面。基本上说,一个手电筒是一个普通的聚光灯,但是根据玩家的位置和方向持续的更新它的位置和方向。 + +所以我们需要为像素着色器提供的值,是聚光灯的位置向量(来计算光的方向坐标),聚光灯的方向向量和遮光角。我们可以把这些值储存在Light结构体中: +```c++ +struct Light +{ + vec3 position; + vec3 direction; + float cutOff; + ... +}; +``` +下面我们把这些适当的值传给着色器: +```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))); +``` +你可以看到,我们为遮光角设置一个角度,但是我们根据一个角度计算了余弦值,把这个余弦结果传给了像素着色器。这么做的原因是在像素着色器中,我们计算LightDir和SpotDir向量的点乘,而点乘返回一个余弦值,不是一个角度,所以我们不能直接把一个角度和余弦值对比。为了获得这个角度,我们必须计算点乘结果的反余弦,这个操作开销是很大的。所以为了节约一些性能,我们先计算给定切光角的余弦值,然后把结果传递给像素着色器。由于每个角度都被表示为余弦了,我们可以直接对比它们,而不用进行任何开销高昂的操作。 + +现在剩下要做的是计算θ值,用它和φ值对比,以决定我们是否在或不在聚光灯的内部: +```c++ +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.0f); +``` +我们首先计算lightDir和负方向向量的点乘(它负的是因为我们想要向量指向光源,而不是从光源作为指向出发点。译注:前面的specular教程中作者却用了相反的表示方法,这里读者可以选择喜欢的表达方式)。确保对所有相关向量进行了归一处理。 + +你可能奇怪为什么if条件中使用>符号而不是<符号。为了在聚光灯以内,θ不是应该比光的遮光值更小吗?这没错,但是不要忘了,角度值是以余弦值来表示的,一个0度的角表示为1.0的余弦值,当一个角是90度的时候被表示为0.0的余弦值,你可以在这里看到: + +![](http://www.learnopengl.com/img/lighting/light_casters_cos.png) + +现在你可以看到,余弦越是接近1.0,角度就越小。这就解释了为什么θ需要比切光值更大了。切光值当前被设置为12.5的余弦,它等于0.9978,所以θ的余弦值在0.9979和1.0之间,片段会在聚光灯内,被照亮。 +运行应用,在聚光灯内的片段才会被照亮。这看起来像这样: + +`[](http://www.learnopengl.com/img/lighting/light_casters_spotlight_hard.png) + +你可以在这里获得全部源码和像素着色器的源码。 + +它看起来仍然有点假,大部分原因是聚光灯有了一个硬边。像素着色器一旦到达了聚光灯的圆锥边缘,它就立刻黑了下来,却没有任何平滑减弱的过度。一个真实的聚光灯的光会在它的边界处平滑减弱的。 + + + +##16.6 平滑/软化边缘 + +为创建聚光灯的平滑边,我们希望去模拟的聚光灯有一个内圆锥和外圆锥。我们可以把内圆锥设置为前面部分定义的圆锥,我们希望外圆锥从内边到外边逐步的变暗。 + +为创建外圆锥,我们简单定义另一个余弦值,它代表聚光灯的方向向量和外圆锥的向量(等于它的半径)的角度。然后,如果片段在内圆锥和外圆锥之间,就会给它计算出一个0.0到1.0之间的亮度。如果片段在内圆锥以内这个亮度就等于1.0,如果在外面就是0.0。 + +我们可以使用下面的公式计算这样的值: + + + + + (2) + + + I + = + + + θ + + γ + + ϵ + + + + + +这里ε是内部(φ)和外部圆锥 + ϵ + = + ϕ + + γ +的差。结果I的值是聚光灯在当前片段的亮度。 + +很难用图画描述出这个公式是怎样工作的,所以我们尝试使用一个例子: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
θθin degreesφ(inner cutoff)φin degreesγ(outer cutoff)γin degreesεI
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.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.966 – 0.953 = 0.04480.966 – 0.953 / 0.0448 = 0.29
+就像你看到的那样我们基本是根据θ在外余弦和内余弦之间插值。如果你仍然不明白怎么继续,不要担心。你可以简单的使用这个公式计算,当你更加老道和明白的时候再来看。 + +由于我们现在有了一个亮度值,当在聚光灯外的时候是个负的,当在内部圆锥以内大于1。如果我们适当地把这个值固定,我们在像素着色器中就再不需要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); +... +// 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结构体,并在应用中设置了它的uniform值。对于下面的图片,内部遮光角12.5f,外部遮光角是17.5f: + +![](http://www.learnopengl.com/img/lighting/light_casters_spotlight.png) + +看起来好多了。仔细看看内部和外部遮光角,尝试创建一个符合你求的聚光灯。可以在这里找到应用源码,以及片段的源代码。 + +这样的一个手电筒/聚光灯类型的灯光非常适合恐怖游戏,结合定向和点光,环境会真的开始被照亮了。下一个教程,我们会结合所有我们目前讨论了的光和技巧。 \ No newline at end of file