OpenGL 投光物 Light casters

Reading time ~1 minute

目前我们使用的所有光照都来自于一个单独的光源。它的效果不错,但是在真实世界,我们有多种类型的光,它们每个表现都不同。一个光源把光投射到物体上,叫做投光。

我们将讨论几种不同的投光类型。分别是定向光(directional light),点光(point light),聚光灯(Spotlight)。

定向光(Directional Light)

当一个光源很远的时候,来自光源的每条光线接近于平行。这看起来就像所有的光线来自于同一个方向,无论物体和观察者在哪儿。当一个光源被设置为无限远时,它被称为定向光(也被成为平行光),因为所有的光线都有着同一个方向。它会独立于光源的位置。

我们知道的定向光源的一个好例子是,太阳。在下面的图片里,来自于太阳的所有的光线都被定义为平行光:

因为所有的光线都是平行的,对于场景中的每个物体光的方向都保持一致,物体和光源的位置保持怎样的关系都无所谓。由于光的方向向量保持一致,光照计算会和场景中的其他物体相似。

通过定义一个光的方向向量,可以模拟这样一个定向光,而不是使用光的位置向量。

为灯光属性定义一个direction,并用它计算灯光向量。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Light
{    
    vec3 direction;
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
...
void main()
{
    vec3 lightDir = normalize(-light.direction);
    ...
}

光照计算需要光的方向是从片段指向光源,而我们通常定义的定向光方向都是从光源出发,所以要更换它的方向,并且执行标准化操作。

注意: 我们目前把光的位置和方向向量传递为vec3,当然你也可以把所有的向量设置为vec4。当定义位置向量为vec4的时候,把w元素设置为1.0非常重要,这样平移和投影才会合理的被应用。然而,当定义一个方向向量为vec4时,我们并不想让平移发挥作用(因为它们除了代表方向,其他什么也不是)所以我们把w元素设置为0.0。

这可以作为简单检查光的类型的方法:若w元素等于1.0,则我们现在所拥有的是光的位置向量;若w等于0.0,则有一个光的方向向量:

1
2
if(lightVector.w == 0.0) // 执行定向光照计算
else if(lightVector.w == 1.0) // 执行顶点光照计算

定点光(Point Light)

定向光作为全局光可以照亮整个场景,但是另一方面除了定向光,我们通常也需要几个定点光,在场景里发亮。点光是一个在时间里有位置的光源,它向所有方向发光,光线随距离增加逐渐变暗。想象灯泡和火炬作为投光物,它们可以扮演点光的角色。

之前我们已经使用了最简单的点光。我们有一个有位置的光源,它从自身的位置向所有方向发出光线。然而,这个我们定义的光源所模拟光线的从不会衰减。

  • 衰减(Attenuation)

    随着光线穿越更远的距离相应地减少亮度,通常被称为衰减(Attenuation)。 一种随着距离减少亮度的方式是使用线性等式。这样的线性方程,可以使远处的物体更暗。然而,线性方程效果会有点假。在真实世界,通常光在近处时非常亮,但是一个光源的亮度,开始的时候减少的非常快,之后随着距离的增加,减少的速度会慢下来。我们需要一种不同的方程来减少光的亮度。

    幸运的是一些聪明人已经早就把它想到了。下面的方程把一个片段的光的亮度除以一个已经计算出来的衰减值,这个值根据光源的远近得到:

    公式中,I是当前片段的光的亮度,d代表片段到光的距离。为了计算衰减,我们定义三个项:常数项Kc,一次项Kl,二次项Kq。

    常数项Kc:通常是1,这样可以保证分母值比1大,因为当比1小反而会增大亮度。 一次项Kl:与距离相乘,会以线性的方式减少亮度。 二次项Kq:与距离的平方相乘,设置一个亮度的二次衰减。

    最终效果就是光在近距离时,非常亮,但是距离变远亮度迅速降低,最后亮度降低速度再次变慢。下面的图展示了在100以内的范围,这样的衰减效果。

    那么,我们该把这三个值设成什么样的值呢。正确值的设置由很多因素决定:环境、你希望光所覆盖的距离范围、光的类型等。大多数场合,这是经验的问题,也要适度调整。

聚光灯(Spotlight)

我们要讨论的最后一种类型光是聚光灯(Spotlight)。聚光灯是一种位于环境中某处的光源,它不是向所有方向照射,而是只朝某个方向照射。结果是只有一个聚光灯照射方向的确定半径内的物体才会被照亮,其他的都保持黑暗。聚光灯的好例子是路灯或手电筒。

OpenGL中的聚光灯用世界空间位置、一个方向和一个指定了聚光灯半径的切光角来表示。我们计算的每个片段,如果片段在聚光灯的切光方向之间(就是在圆锥体内),我们就会把片段照亮。下面的图可以让你明白聚光灯是如何工作的:

  • LightDir:从片段指向光源的向量。
  • SpotDir:聚光灯所指向的方向。
  • φ:定义聚光灯半径的切光角。每个落在这个角度之外的,聚光灯都不会照亮。
  • θ:LightDir向量和SpotDir向量之间的角度。θ值应该比φ值小,这样才会在聚光灯内。

所以我们大致要做的是,计算LightDir向量和SpotDir向量的点乘,然后在和遮光角φ对比。

  • 平滑/软化边缘

    为创建聚光灯的平滑边,我们希望去模拟的聚光灯有一个内圆锥和外圆锥。我们可以把内圆锥设置为前面定义的圆锥,我们希望外圆锥从内边到外边逐步的变暗。
    为创建外圆锥,我们简单定义另一个余弦值,它代表聚光灯的方向向量和外圆锥的向量(等于它的半径)的角度。然后,如果片段在内圆锥和外圆锥之间,就会给它计算出一个0.0到1.0之间的亮度。如果片段在内圆锥以内这个亮度就等于1.0,如果在外面就是0.0。


参考 LearnOpenGL-CN 投光物

Scriptable Objects 及 游戏架构

Scriptable Objects 相关介绍,及基于其的游戏架构技术 Continue reading

AssetBundle 最佳实践

Published on January 29, 2019

AssetBundle 基础总结

Published on January 27, 2019