diff --git a/source/10-let-there-be-light.md b/source/10-let-there-be-light.md index f19606b..4861dc5 100644 --- a/source/10-let-there-be-light.md +++ b/source/10-let-there-be-light.md @@ -1,40 +1,35 @@ # 要有光(Let there be light) -在本章中,我们将学习如何为我们的3D游戏引擎添加光照。 我们不会去实现一个完美的物理光照模型,因为抛开复杂性不说,它还需要巨量的计算机资源,相反我们只需要一个近似的、像样的光照效果。 我们将使用一种名为 __Phong__ 的着色算法(由Bui Tuong Phong开发)。 另一个需要注意的是,我们将只模拟灯光,但我们不会模拟这些灯光所产生的阴影(这将在其他章节中完成)。 +在本章中,我们将学习如何为我们的3D游戏引擎添加光照。我们不会去实现一个完美的物理光照模型,因为抛开复杂性不说,它还需要巨量的计算机资源,相反我们只需要一个近似的、像样的光照效果。我们将使用一种名为 __Phong__ 的着色算法(由Bui Tuong Phong开发)。另一个需要注意的是,我们将只模拟灯光,但我们不会模拟这些灯光所产生的阴影(这将在其他章节中完成)。 在开始之前,首先定义几个光源种类: * **点光源(Point Light)**:这种光源模拟的是一个由点向空间各个方向均匀散射的光源 - * **聚光源(Spot Light)**:这种光源模拟从空间中的点发射的光源,但不是在所有方向上发射,而是限定在了一个锥形方向上 - -* **平行光源(Directional Light)**:这种光源模拟了太阳光,3D场景中的所有物体都会受到来自特定方向的平行光线的照射。 无论物体是近抑或是远,光线都是以一定角度照射在物体上的。 - -* **环境光(Ambient Light)**:这种类型的光源来自空间的任何地方,并以相同的方式照亮所有物体。 +* **平行光源(Directional Light)**:这种光源模拟了太阳光,3D场景中的所有物体都会受到来自特定方向的平行光线的照射。无论物体是近抑或是远,光线总是以一定角度照射在物体上的。 +* **环境光(Ambient Light)**:这种类型的光源来自空间的任何地方,并以相同的强度照亮所有物体。 ![光照类型](_static/10/light_types.png) -因此,为了模拟光,我们需要考虑光源类型,以及光的位置和其他一些参数,如颜色。 当然,我们还必须考虑物体如何受光照影响及吸收和反射光。 +因此,为了模拟光,我们需要考虑光源类型,以及光的位置和其他一些参数,如颜色。当然,我们还必须考虑物体如何受光照影响以及吸收和反射光。 -Phong着色算法将模拟光线对我们模型中每个点的影响,即每个顶点的影响。 这就是为什么它被称为局部光照模型的原因,这也是该算法不能计算阴影的原因,它只会计算应用到每个顶点的光,而不考虑顶点是否在挡光物体的后面。 我们将在后面的章节中解决这个问题。 但是,正因为如此,它是一种非常简单快速的算法,并且可以提供非常好的效果。 我们将在这里使用一个没有深入考虑材质的简化版本。 +Phong着色算法将模拟光线对我们模型中每个点的影响,即对每个顶点的影响。这就是为什么它被称为局部光照模型的原因,这也是该算法不能计算阴影的原因,它只会计算应用到每个顶点的光,而不考虑顶点是否在挡光物体的后面。我们将在后面的章节中解决这个问题。但是,正因为如此,它是一种非常简单快速的算法,并且可以提供非常好的效果。我们将在这里实现一个没有深入考虑材质影响的简化版本。 Phong算法提供了三种光照分量: * **环境光(Ambient Light)**:模拟来自任何地方的光,这将为我们提供(需要对应强度值)未被任何光线照射的区域,就像背景光。 - * **漫反射(Diffuse Reflectance)**:考虑到面对光源的表面更亮。 - * **镜面反射(Specular Reflectance)**:模拟光线如何在抛光表面或金属表面上反射。 -最后,我们还要知道的规律是,乘以分配给片段的颜色,将根据接收的光线将该颜色变得更亮或更暗。我们令$A$为环境组光、$D$为漫反射、$S$为高光。 以上规律对于分量的加法表示如下: +最后,我们还要知道的规律是,乘以分配给片段的颜色,将根据接收的光线将该颜色变得更亮或更暗。我们令$A$为环境光、$D$为漫反射、$S$为镜面高光。根据以上规律对于分量的加法表示如下: $$L = A + D + S$$ -这些分量其实就是颜色,也就是每个光分量所贡献的颜色分量。 这是因为光分量不仅会提供一定程度的强度,还会改变模型的颜色。在我们的片段着色器中,我们只需将该光的颜色与原始片段颜色(从纹理或基色获得)相乘即可。 +这些分量其实就是颜色,也就是每个光分量所贡献的颜色分量。这是因为光分量不仅会提供一定程度的强度,还会改变模型的颜色。在我们的片段着色器中,我们只需将该光的颜色与原始片段颜色(从纹理或基色获得)相乘即可。 -我们也可以为相同的材质分配不同的颜色,这些颜色将用于环境,漫反射和镜像反射。 因此,这些分量将由材质相关的颜色而受到调整。 如果材质具有纹理,我们将简单地为每个分量使用单个纹理。 +我们也可以为相同的材质分配不同的颜色,这些颜色将用于环境光,漫反射和镜面反射。因此,这些分量将由材质相关的颜色而受到调整。如果材质具有纹理,我们将简单地为每个分量使用单个纹理。 -所以对于非纹理材质的最终颜色将是:$L = A * 环境光色 + D * 漫反射的颜色 + S * 高光颜色$ +所以对于非纹理材质的最终颜色将是:$L = A * 环境光色 + D * 漫反射的颜色 + S * 镜面反射的颜色$ 对于有纹理材质的最终颜色将是: @@ -42,41 +37,41 @@ $$L = A * 材质颜色 + D * 材质颜色 + S * 材质颜色$$ ## 环境光分量 -让我们来看看第一个分量,即环境光分量,它只是一个常量值,会使我们的所有对象变得更亮或更暗。 我们可以使用它来模拟特定时间段内的光线(黎明,黄昏等),也可以用它来添加一些光线,这些光线不直接被光线照射,但可以以简单的方式被间接光线照射(比如反射)。 +让我们来看看第一个分量,即环境光分量,它只是一个常量值,会使我们的所有对象变得更亮或更暗。我们可以使用它来模拟特定时间段内的光线(黎明,黄昏等),也可以用它来添加一些光线,这些光线不直接被光线照射,但可以以简单的方式被间接光线照射(比如反射)。 -环境光是运算最简单的分量,我们只需要传递一种颜色,并乘以基本颜色,以调整该基本颜色。 假如我们已经确定片段的颜色是$(1.0,0.0,0.0)$,即红色。 如果没有环境光时,它将显示为完全红色的片段。 如果我们将环境光设置为$(0.5,0.5,0.5)$,则最终颜色将为$(0.5,0,0)$,其实就是变暗的红色。 这种光会以同样的方式使所有片段变暗(说光照暗了物体似乎有点奇怪,实际上这就是我们得到的效果)。除此之外,如果光色的RGB分量不相同,它还可以为片段添加一些颜色,所以我们只需要一个向量来调节环境光强度和颜色。 +环境光是运算最简单的分量,我们只需要传递一种颜色,并乘以基本颜色,以调整该基本颜色。假如我们已经确定片段的颜色是$(1.0,0.0,0.0)$,即红色。如果没有环境光时,它将显示为完全红色的片段。如果我们将环境光设置为$(0.5,0.5,0.5)$,则最终颜色将为$(0.5,0,0)$,其实就是变暗的红色。这种光会以同样的方式使所有片段变暗(说光照暗了物体似乎有点奇怪,实际上这就是我们得到的效果)。除此之外,如果光色的RGB分量不相同,它还可以为片段添加一些颜色,所以我们只需要一个向量来调节环境光强度和颜色。 ## 漫反射 -现在我们来谈谈漫反射。 它模拟了这样的规律,即与光源垂直的面看起来比以更接近光的角度接收光的面更亮。 一个物体接收的光线越多,其光密度(让我这样称呼)就越高。 +现在我们来谈谈漫反射。它模拟了这样的规律,即与光源垂直的面看起来比以更接近光的角度接收光的面更亮。一个物体接收的光线越多,其光密度(让我这样称呼)就越高。 ![漫反射光](_static/10/diffuse_light.png) -但是,我们该如何计算它? 你还记得上一章我们介绍过的法线概念吗? 法线是垂直于平面并且长度为1的向量。 因此,让我们在上图中绘制三个点的法线,如你所见,每个点的法线将是垂直于每个点的切平面的向量。 我们不去绘制来自光源的光线,而是绘制从每个点到光源(即相反的方向)的向量。 +但是,我们该如何计算它? 你还记得上一章我们介绍过的法线概念吗?法线是垂直于平面并且长度为1的向量。因此,让我们在上图中绘制三个点的法线,如你所见,每个点的法线将是垂直于每个点的切平面的向量。我们不去绘制来自光源的光线,而是绘制从每个点到光源(即相反的方向)的向量。 ![法线与光的方向](_static/10/diffuse_light_normals.png) -正如你所看到的,$P1$点的法线$N1$,与指向光源的向量平行,该法线的方向与光线的方向相反($N1$已经被平移标记,以便你可以看到它,但它在数学上是等价的)。$P1$相对于指向光源的向量,其角度等于$0$。 因为它的平面垂直于光源,所以$P1$将是最亮的点。 +正如你所看到的,$P1$点的法线$N1$,与指向光源的向量平行,该法线的方向与光线的方向相反($N1$已经被平移标记,以便你可以看到它,但它在数学上是等价的)。$P1$相对于指向光源的向量,其角度等于$0$。因为它的平面垂直于光源,所以$P1$将是最亮的点。 -$P2$点的法线$N2$,与指向光源的向量的夹角约为30度,所以它应该比$P1$更暗。最后,$P3$的法线$N3$也与指向光源的向量平行,但两个向量的方向相反。 $P3$与指向光源的向量的角度为180度,所以根本不应该获得任何光线。 +$P2$点的法线$N2$,与指向光源的向量的夹角约为30度,所以它应该比$P1$更暗。最后,$P3$的法线$N3$也与指向光源的向量平行,但两个向量的方向相反。$P3$与指向光源的向量的角度为180度,所以根本不应该获得任何光线。 所以,看起来我们得到了一个计算某个点的光照强度的好方法,光强与该点的法线和该点指向光源的向量之间的夹角大小有关。但我们具体要怎么计算它呢? -有一个我们可以使用的数学运算————点积。 该操作需要两个向量并产生一个数字(标量),如果它们之间的角度较小,则生成一个正数;如果它们之间的角度很大,则生成一个负数。 如果两个向量都被归一化,即两者的长度都等于1,那么点积的结果将介于$-1$和$1$之间。 如果两个向量的方向相同(即夹角为$0$),则点积为1;如果两个向量夹角为直角,则它的值为$0$;如果两个向量的方向相反,则为$-1$。 +有一个我们可以使用的数学运算————数量积(又称为点积)。该操作需要两个向量并产生一个数字(标量),如果它们之间的角度较小,则生成一个正数;如果它们之间的角度很大,则生成一个负数。如果两个向量都被归一化,即两者的长度都等于1,那么数量积的结果将介于$-1$和$1$之间。如果两个向量的方向相同(即夹角为$0$),则数量积为1;如果两个向量夹角为直角,则它的值为$0$;如果两个向量的方向相反,则为$-1$。 -我们定义两个向量,$v1$和$v2$,并以$$α$$作为它们之间的夹角。点积的定义如下: +我们定义两个向量,$v1$和$v2$,并以$$α$$作为它们之间的夹角。数量积的定义如下: -![点积](_static/10/dot_product.png) +![数量积](_static/10/dot_product.png) -如果两个向量都归一化,即它们的长度,它们的模块将等于1,它们的点积即为夹角的余弦值。 我们同样使用该运算来计算漫反射分量。 +如果两个向量都归一化,即它们的长度,它们的模块将等于1,它们的数量积即为夹角的余弦值。我们同样使用该运算来计算漫反射分量。 -所以我们需要计算指向光源的向量。 我们如何做到这一点? 假如我们有每个点的位置(即顶点位置),我们有光源的位置。首先,这两个坐标必须位于相同的坐标系中。 为了简化,让我们假设它们都处于世界坐标系中,那么这些位置是指向顶点位置($VP$)和光源($VS$)的矢量的坐标,如下图所示: +所以我们需要计算指向光源的向量。我们如何做到这一点? 假如我们有每个点的位置(即顶点位置),我们有光源的位置。首先,这两个坐标必须位于相同的坐标系中。为了简化,让我们假设它们都处于世界坐标系中,那么这些位置是指向顶点位置($VP$)和光源($VS$)的矢量的坐标,如下图所示: ![漫反射光照运算](_static/10/diffuse_calc_i.png) 如果我们从$VP$中减去$VS$,我们就得到了$L$向量。 -现在,我们可以在指向光源的矢量和法线之间做点积,因为Johann Lambert是第一个提出这种关系来模拟平面亮度的,所以该乘积被称为兰伯特项。 +现在,我们可以在指向光源的矢量和法线之间做数量积,因为Johann Lambert是第一个提出这种关系来模拟平面亮度的,所以该乘积被称为兰伯特项。 让我们总结一下,我们定义以下变量: @@ -88,19 +83,19 @@ $P2$点的法线$N2$,与指向光源的向量的夹角约为30度,所以它 首先,我们需要计算从当前位置指向光源的向量:$toLightDirection = lPos - vPos$。该操作的结果需要进行归一化。 -然后我们需要计算漫反射因子(标量):$diffuseFactor = normal \cdot toLightDirection$。计算两个向量之间的点积,我们希望它在$-1$和$1$之间,所以两个向量都需要进行归一化。颜色需要在$0$到$1$之间,所以如果值低于$0$,我们将它设置为$0$。 +然后我们需要计算漫反射因子(标量):$diffuseFactor = normal \cdot toLightDirection$。计算两个向量之间的数量积,我们希望它在$-1$和$1$之间,所以两个向量都需要进行归一化。颜色需要在$0$到$1$之间,所以如果值低于$0$,我们将它设置为$0$。 最后,我们只需要通过漫反射因子和光强来调制光色: $$ color = diffuseColour * lColour * diffuseFactor * intensity$$ -## 镜像反射 +## 镜面反射 -现在我们来看看镜像反射,但首先我们需要知道光线是如何反射的。 当光照射到一个平面时,它的一部分被吸收,另一部分被反射,如果你还记得你的物理课内容,反射就是光子从物体反弹回来。 +现在我们来看看镜面反射,但首先我们需要知道光线是如何反射的。当光照射到一个平面时,它的一部分被吸收,另一部分被反射,如果你还记得你的物理课内容,反射就是光子从物体反弹回来。 ![反射光](_static/10/light_reflection.png) -当然,平面不是完全抛光的,如果你近距离仔细观察,你会看到很多不平整的地方。 除此之外,有许多射线光(实际上是光子),会撞击这个平面,并且会以各种各样的角度进行反射。 因此,我们看到的就像是一束光照射一平面并散射出去。 也就是说,光线在撞击平面时会发散,这就是我们之前讨论过的漫反射分量。 +当然,平面不是完全抛光的,如果你近距离仔细观察,你会看到很多不平整的地方。除此之外,有许多射线光(实际上是光子),会撞击这个平面,并且会以各种各样的角度进行反射。因此,我们看到的就像是一束光照射一平面并散射出去。也就是说,光线在撞击平面时会发散,这就是我们之前讨论过的漫反射分量。 ![平面](_static/10/surface.png) @@ -108,19 +103,19 @@ $$ color = diffuseColour * lColour * diffuseFactor * intensity$$ ![抛光平面](_static/10/polished_surface.png) -这就是镜像反射模型,它取决于材质特性。关于镜面反射,要注意的一点是,只有当摄像机处于适当的位置时,即反射光的发射区域内,反射光才可见。 +这就是镜面反射模型,它取决于材质特性。关于镜面反射,要注意的一点是,只有当摄像机处于适当的位置时,即反射光的发射区域内,反射光才可见。 -![高光](_static/10/specular_lightining.png) +![镜面高光](_static/10/specular_lightining.png) -解释了反射的机制,我们接下来准备计算这个分量。首先,我们需要一个从光源指向顶点的向量。当我们计算漫反射分量时,我们使用的是方向与之相反的向量,它指向的是光源。 $toLightDirection$,所以让我们将其计算为$fromLightDirection = -(toLightDirection)$。 +解释了反射的机制,我们接下来准备计算这个分量。首先,我们需要一个从光源指向顶点的向量。当我们计算漫反射分量时,我们使用的是方向与之相反的向量,它指向的是光源。$toLightDirection$,所以让我们将其计算为$fromLightDirection = -(toLightDirection)$。 然后我们需要计算正常情况下由$fromLightDirection$到平面所产生的反射光。有一个名为reflect的GLSL函数。所以,$reflectLight = reflect(fromLightSource, normal)$。 我们还需要一个指向相机的向量,并将其命名为$cameraDirection$,然后计算出相机位置和顶点位置之间的差值:$cameraDirection = cameraPos - vPos$。相机位置向量和顶点位置需要处于相同的坐标系中,并且生成的向量需要进行归一化。下图概述了我们目前计算的主要分量: -![高光运算](_static/10/specular_lightining_calc.png) +![镜面高光运算](_static/10/specular_lightining_calc.png) -现在我们需要计算光强,即$specularFactor$。如果$cameraDirection$和$reflectLight$向量指向相同的方向,该值就越高,如果它们方向相反其值则越低。为了计算这个值我们将再次使用点积。$specularFactor = cameraDirection \cdot reflectLight$。我们只希望这个值在$0$和$1$之间,所以如果它低于$0$,就设置它为0。 +现在我们需要计算光强,即$specularFactor$。如果$cameraDirection$和$reflectLight$向量指向相同的方向,该值就越高,如果它们方向相反其值则越低。为了计算这个值我们将再次使用数量积。$specularFactor = cameraDirection \cdot reflectLight$。我们只希望这个值在$0$和$1$之间,所以如果它低于$0$,就设置它为0。 我们还需要考虑到,如果相机指向反射光锥,则该光更强烈。这可以通过计算$specularFactor$的$specularPower$幂来实现,其中$specularPower$为给定的参数: @@ -130,7 +125,7 @@ $$ specularFactor = specularFactor ^ {specularPower} $$。 ## 衰减 -我们现在知道如何计算这三个分量了,这些分量可以帮助我们用环境光模拟点光源。 但是我们的光照模型还不完整,物体反射的光与光的距离无关,我们需要模拟光线衰减。 +我们现在知道如何计算这三个分量了,这些分量可以帮助我们用环境光模拟点光源。但是我们的光照模型还不完整,物体反射的光与光的距离无关,我们需要模拟光线衰减。 衰减是一个有关距离和光的函数。光的强度与距离的平方成反比。这很容易理解,随着光线的传播,其能量沿着球体表面分布,其半径等于光线行进的距离,而球的表面与其半径的平方成正比。我们可以用下式来计算衰减因子:$1.0 /(atConstant + atLineardist + atExponentdist ^ {2})$。 @@ -210,7 +205,7 @@ struct Material * 用于环境分量的颜色。 * 用于漫反射分量的颜色。 -* 用于镜像反射的颜色。 +* 用于镜面反射的颜色。 材质也由一个标志来定义,该标志控制它是否具有相关的纹理以及反射率指数。我们将在片段着色器中使用以下uniform。 @@ -226,12 +221,12 @@ uniform vec3 camera_pos; 我们用新建的uniform设置下面的几个变量: * 环境光:包含会以同样方式影响每个片段的颜色。 -* 高光反射功率(在讨论高光反射光时提供的公式中使用的指数)。 +* 镜面反射强度(在讨论镜面反射光时提供的公式中使用的指数)。 * 一个点光源。 * 材质特性。 * 相机在视图空间坐标系中的位置。 -我们还将定义一些全局变量,它们将保存要在环境、漫反射和镜像反射中使用的材质颜色分量。我们使用这些变量是因为如果分量具有纹理,我们将对所有分量使用相同的颜色,并且我们不希望进行冗余的纹理查找。这些变量是这样定义的: +我们还将定义一些全局变量,它们将保存要在环境、漫反射和镜面反射中使用的材质颜色分量。我们使用这些变量是因为如果分量具有纹理,我们将对所有分量使用相同的颜色,并且我们不希望进行冗余的纹理查找。这些变量是这样定义的: ```glsl vec4 ambientC; @@ -259,7 +254,7 @@ void setupColours(Material material, vec2 textCoord) } ``` -现在我们要定义一个函数,以点光源、顶点位置及其法线为输入并返回前面描述的漫反射和镜像反射计算的颜色。 +现在我们要定义一个函数,以点光源、顶点位置及其法线为输入并返回前面描述的漫反射和镜面反射计算的颜色。 ```glsl vec4 calcPointLight(PointLight light, vec3 position, vec3 normal) @@ -273,7 +268,7 @@ vec4 calcPointLight(PointLight light, vec3 position, vec3 normal) float diffuseFactor = max(dot(normal, to_light_source ), 0.0); diffuseColour = diffuseC * vec4(light.colour, 1.0) * light.intensity * diffuseFactor; - // 高光 + // 镜面反射 vec3 camera_direction = normalize(-position); vec3 from_light_source = -to_light_source; vec3 reflected_light = normalize(reflect(from_light_source, normal)); @@ -289,9 +284,9 @@ vec4 calcPointLight(PointLight light, vec3 position, vec3 normal) } ``` -前面的代码相对比较直白简单,它只是计算了漫反射分量的颜色,另一个是计算镜像反射的颜色,并通过光线在行进到我们正在处理的顶点时受到的衰减来调制它们。 +前面的代码相对比较直白简单,它只是计算了漫反射分量的颜色,另一个是计算镜面反射的颜色,并通过光线在行进到我们正在处理的顶点时受到的衰减来调制它们。 -请注意,顶点坐标是位于视图空间中的。在计算镜像反射时,我们必须指出视角,即相机。这可以这样做: +请注意,顶点坐标是位于视图空间中的。在计算镜面反射时,我们必须指出视角,即相机。这可以这样做: ```glsl vec3 camera_direction = normalize(camera_pos - position); @@ -322,7 +317,7 @@ void main() } ``` -调用setupColours函数将使用适当的颜色来设置变量ambientC、diffuseC和speculrC。然后,我们计算漫反射和镜像反射,并考虑衰减。为了方便,我们使用单个函数调用来完成此操作,如上所述。最终的颜色是通过添加环境光分量来计算的(将ambientC乘以环境光)。正如你所看到的,环境光不受衰减的影响。 +调用setupColours函数将使用适当的颜色来设置变量ambientC、diffuseC和speculrC。然后,我们计算漫反射和镜面反射,并考虑衰减。为了方便,我们使用单个函数调用来完成此操作,如上所述。最终的颜色是通过添加环境光分量来计算的(将ambientC乘以环境光)。正如你所看到的,环境光不受衰减的影响。 在着色器中我们引入了一些需要进一步解释的新概念,定义结构体并将它们用作uniform。但我们要怎么传递这些结构体?首先,我们将定义两个新类,它们模拟点点光源和材质的特性,名为`PointLight`和`Material`。它们只是普通的POJO(普通的Java对象),所以你可以在本书附带的源代码中查看它们。然后,我们需要在ShaderProgram类中创建新方法,首先要能够为点光源和材质结构创建uniform。