2021SC@SDUSC
目录
2.4CalculateAmbientBoxLightRadius
OvRendering中剩余的重要内容已经不多,本节将讨论light(灯光)的相关内容。但在讨论代码之前,我们需要先了解一下投光物相关的知识。
1.投光物
我们目前使用的光照都来自于空间中的一个点。它能给我们不错的效果,但现实世界中,我们有很多种类的光照,每种的表现都不同。将光投射(Cast)到物体的光源叫做投光物(Light Caster)。
1.1平行光
当一个光源处于很远的地方时,来自光源的每条光线就会近似于互相平行。不论物体和/或者观察者的位置,看起来好像所有的光都来自于同一个方向。当我们使用一个假设光源处于无限远处的模型时,它就被称为定向光,因为它的所有光线都有着相同的方向,它与光源的位置是没有关系的。
定向光非常好的一个例子就是太阳。太阳距离我们并不是无限远,但它已经远到在光照计算中可以把它视为无限远了。所以来自太阳的所有光线将被模拟为平行光线,我们可以在下图看到:
因为所有的光线都是平行的,所以物体与光源的相对位置是不重要的,因为对场景中每一个物体光的方向都是一致的。由于光的位置向量保持一致,场景中每个物体的光照计算将会是类似的。
1.2点光源
定向光对于照亮整个场景的全局光源是非常棒的,但除了定向光之外我们也需要一些分散在场景中的点光源(Point Light)。点光源是处于世界中某一个位置的光源,它会朝着所有方向发光,但光线会随着距离逐渐衰减。想象作为投光物的灯泡和火把,它们都是点光源。
1.3衰减
随着光线传播距离的增长逐渐削减光的强度通常叫做衰减(Attenuation)。随距离减少光强度的一种方式是使用一个线性方程。这样的方程能够随着距离的增长线性地减少光的强度,从而让远处的物体更暗。然而,这样的线性方程通常会看起来比较假。
在现实世界中,灯在近处通常会非常亮,但随着距离的增加光源的亮度一开始会下降非常快,但在远处时剩余的光强度就会下降的非常缓慢了。所以,我们需要一个不同的公式来减少光的强度。
幸运的是一些聪明的人已经帮我们解决了这个问题。下面这个公式根据片段距光源的距离计算了衰减值,之后我们会将它乘以光的强度向量:
在这里d代表了片段距光源的距离。接下来为了计算衰减值,我们定义3个(可配置的)项:常数项Kc、一次项Kl和二次项Kq。
- 常数项通常保持为1.0,它的主要作用是保证分母永远不会比1小,否则的话在某些距离上它反而会增加强度,这肯定不是我们想要的效果。
- 一次项会与距离值相乘,以线性的方式减少强度。
- 二次项会与距离的平方相乘,让光源以二次递减的方式减少强度。二次项在距离比较小的时候影响会比一次项小很多,但当距离值比较大的时候它就会比一次项更大了。
由于二次项的存在,光线会在大部分时候以线性的方式衰退,直到距离变得足够大,让二次项超过一次项,光的强度会以更快的速度下降。这样的结果就是,光在近距离时亮度很高,但随着距离变远亮度迅速降低,最后会以更慢的速度减少亮度。下面这张图显示了在100的距离内衰减的效果:
你可以看到光在近距离的时候有着最高的强度,但随着距离增长,它的强度明显减弱,并缓慢地在距离大约100的时候强度接近0。这正是我们想要的。
选择正确的值
但是,该对这三个项设置什么值呢?正确地设定它们的值取决于很多因素:环境、希望光覆盖的距离、光的类型等。在大多数情况下,这都是经验的问题,以及适量的调整。下面这个表格显示了模拟一个(大概)真实的,覆盖特定半径(距离)的光源时,这些项可能取的一些值。第一列指定的是在给定的三项时,光所能覆盖的距离。这些值是大多数光源很好的起始点,它们由Ogre3D的Wiki所提供:
距离 | 常数项 | 一次项 | 二次项 |
---|---|---|---|
7 | 1.0 | 0.7 | 1.8 |
13 | 1.0 | 0.35 | 0.44 |
20 | 1.0 | 0.22 | 0.20 |
32 | 1.0 | 0.14 | 0.07 |
50 | 1.0 | 0.09 | 0.032 |
65 | 1.0 | 0.07 | 0.017 |
100 | 1.0 | 0.045 | 0.0075 |
160 | 1.0 | 0.027 | 0.0028 |
200 | 1.0 | 0.022 | 0.0019 |
325 | 1.0 | 0.014 | 0.0007 |
600 | 1.0 | 0.007 | 0.0002 |
3250 | 1.0 | 0.0014 | 0.000007 |
你可以看到,常数项Kc在所有的情况下都是1.0。一次项Kl为了覆盖更远的距离通常都很小,二次项Kq甚至更小。
1.4聚光
我们要讨论的最后一种类型的光是聚光(Spotlight)。聚光是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。这样的结果就是只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗。聚光很好的例子就是路灯或手电筒。
OpenGL中聚光是用一个世界空间位置、一个方向和一个切光角(Cutoff Angle)来表示的,切光角指定了聚光的半径(译注:是圆锥的半径不是距光源距离那个半径)。对于每个片段,我们会计算片段是否位于聚光的切光方向之间(也就是在锥形内),如果是的话,我们就会相应地照亮片段。下面这张图会让你明白聚光是如何工作的:
LightDir
:从片段指向光源的向量。SpotDir
:聚光所指向的方向。Phi
ϕ:指定了聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。Theta
θ:LightDir向量和SpotDir向量之间的夹角。在聚光内部的话θ值应该比ϕ值小。
所以我们要做的就是计算LightDir向量和SpotDir向量之间的点积,并将它与切光角ϕ值对比。
1.5平滑/软化边缘
为了创建一种看起来边缘平滑的聚光,我们需要模拟聚光有一个内圆锥(Inner Cone)和一个外圆锥(Outer Cone)。我们可以将内圆锥设置为上文提到的那个圆锥,但我们也需要一个外圆锥,来让光从内圆锥逐渐减暗,直到外圆锥的边界。
为了创建一个外圆锥,我们只需要再定义一个余弦值来代表聚光方向向量和外圆锥向量(等于它的半径)的夹角。然后,如果一个片段处于内外圆锥之间,将会给它计算出一个0.0到1.0之间的强度值。如果片段在内圆锥之内,它的强度就是1.0,如果在外圆锥之外强度值就是0.0。
我们可以用下面这个公式来计算这个值(下列公式中以角度代表余弦值):
这里ϵ(Epsilon)等于内圆锥(ϕ)和外圆锥(γ)之间的余弦值差。最终的I值就是在当前片段聚光的强度。
很难去表现这个公式是怎么工作的,所以我们用一些实例值来看看:
2.Light
了解了上述内容,能够帮助我们更好地理解代码的实现。
Light并不是一个C++类,而是一个结构体,所以它的结构相对简单一些,首先在开头它定义了枚举变量表示灯光的类型:POINT(点光源), DIRECTIONAL(平行光), SPOT(聚光), AMBIENT_BOX(立方体环境光), AMBIENT_SPHERE(球型环境光)。
之后,定义默认的灯光颜色为(1,1,1)即白色,以及与灯光相关的一系列参数:intensity(基础光照强度)、constant(光强衰减系数的常数部分)、linear(光强衰减系数的一次部分)、quadratic(光强衰减系数的二次部分)、cutoff(内圆锥的角度半径)、outerCutoff(外圆锥的角度半径)以及光源枚举类型的浮点数表示,默认状态下为点光源。
最后还有一个光源的位置变换对象。
struct Light
{
enum class Type { POINT, DIRECTIONAL, SPOT, AMBIENT_BOX, AMBIENT_SPHERE };
OvMaths::FVector3 color = { 1.f, 1.f, 1.f };
float intensity = 1.f;
float constant = 0.0f;
float linear = 0.0f;
float quadratic = 1.0f;
float cutoff = 12.f;
float outerCutoff = 15.f;
float type = 0.0f;
protected:
OvMaths::FTransform& m_transform;
};
以下是其相关的函数,我们只看简要看看GenerateMatrix函数。
Light(OvMaths::FTransform& p_tranform, Type p_type);
OvMaths::FMatrix4 GenerateMatrix() const;
float GetEffectRange() const;
const OvMaths::FTransform& GetTransform() const;
2.1GenerateMatrix
OvMaths::FMatrix4 OvRendering::Entities::Light::GenerateMatrix() const
{
OvMaths::FMatrix4 result;
auto position = m_transform.GetWorldPosition();
result.data[0] = position.x;
result.data[1] = position.y;
result.data[2] = position.z;
auto forward = m_transform.GetWorldForward();
result.data[4] = forward.x;
result.data[5] = forward.y;
result.data[6] = forward.z;
result.data[8] = static_cast<float>(Pack(color));
result.data[12] = type;
result.data[13] = cutoff;
result.data[14] = outerCutoff;
result.data[3] = constant;
result.data[7] = linear;
result.data[11] = quadratic;
result.data[15] = intensity;
return result;
}
GenerateMatrix将返回light的所有参数组合成的4维矩阵,它可以表示为以下形式:
position.x | position.y | position.z | constant |
forward.x | forward.y | forward.z | linear |
color | quadratic | ||
type | cutoff | outerCutoff | intensity |
我们可以看到在计算color的时候使用了一个函数Pack,接下来我们来看看它的具体内容。
2.2Pack
uint32_t Pack(const OvMaths::FVector3& p_toPack)
{
return Pack(static_cast<uint8_t>(p_toPack.x * 255.f), static_cast<uint8_t>(p_toPack.y * 255.f), static_cast<uint8_t>(p_toPack.z * 255.f), 0);
}
uint32_t Pack(uint8_t c0, uint8_t c1, uint8_t c2, uint8_t c3)
{
return (c0 << 24) | (c1 << 16) | (c2 << 8) | c3;
}
以上两个函数属于Light.cpp,但并不作为结构体内部的函数,外层的pack函数需要一个三维向量的颜色值,由于我们定义color时采用的是0~1的范围,所以在这里需要映射到0~255的范围,然后我们还要将浮点数转化为8bit的整型,并传入内层的pack函数。
内层的pack函数除了rgb三个值外,为了方便4维运算还默认传入了一个0,这也是为什么GenerateMatrix有3个空位存储color值,却仍然进行pack操作的原因。
得到四个参数后,首先对其分别做按位左移操作,这样呈现以下状态:
c0=xxxxxxxx 00000000 00000000 00000000
c1=00000000 xxxxxxxx 00000000 00000000
c2=00000000 00000000 xxxxxxxx 00000000
c3=00000000 00000000 00000000 xxxxxxxx
最后对这个4个数进行按位或操作,就会得到一个包含四个变量的32位整型颜色值。
2.3CalculatePointLightRadius
在讨论CalculatePointLightRadius函数前先来简单看看衰减计算函数CalculateLuminosity,它的实现过程遵循了光照强度的衰减公式。
float CalculateLuminosity(float p_constant, float p_linear, float p_quadratic, float p_intensity, float p_distance)
{
auto attenuation = (p_constant + p_linear * p_distance + p_quadratic * (p_distance * p_distance));
return (1.0f / attenuation) * std::abs(p_intensity);
}
接下来就来看CalculatePointLightRadius的实现过程。为了计算点光源的有效照明半径,除了衰减系数以外,我们还需要3个变量:
threshold:最小光照强度阈值,光强低于这个值则认为无光强;
step:寻找最近似半径时的遍历步长;
distance:点光源覆盖半径距离。
float CalculatePointLightRadius(float p_constant, float p_linear, float p_quadratic, float p_intensity)
{
constexpr float threshold = 1 / 255.0f;
constexpr float step = 1.0f;
float distance = 0.0f;
接下来,函数定义了2种宏TRY_GREATER与TRY_LESS。
其中,TRY_GREATER进行光照半径的上边界判断,若在当前距离参数value下计算CalculateLuminosity的值大于阈值,则value<=最大半径,将value赋值给distance;
同样,TRY_LESS进行光照半径的下边界判断,除了当前距离之外,还需要传入新的距离值。若在当前距离参数value下计算CalculateLuminosity的值小于阈值,则value>最大半径,将更小的新的距离值newValue赋值给distance。
#define TRY_GREATER(value)\
else if (CalculateLuminosity(p_constant, p_linear, p_quadratic, p_intensity, value) > threshold)\
{\
distance = value;\
}
#define TRY_LESS(value, newValue)\
else if (CalculateLuminosity(p_constant, p_linear, p_quadratic, p_intensity, value) < threshold)\
{\
distance = newValue;\
}
完成了这两个定义后,就是具体的判断过程,首先我们定义1000为半径最大值,若计算后的光强仍大于阈值,我们判定点光源的覆盖半径为无穷大;
若上述情况不成立,我们调用上述的两个宏,利用20测试下边界,若小于阈值,则半径<20,并将distance赋值为0;反之半径>=20,则利用750测试上边界,若大于阈值,则半径>=750,distance赋值为750,反之半径<750,如此进行多次判定,最后得到一个较为近似的distance。
// Prevents infinite while true. If a light has a bigger radius than 10000 we ignore it and make it infinite
if (CalculateLuminosity(p_constant, p_linear, p_quadratic, p_intensity, 1000.0f) > threshold)
{
return std::numeric_limits<float>::infinity();
}
TRY_LESS(20.0f, 0.0f)
TRY_GREATER(750.0f)
TRY_LESS(50.0f, 20.0f + step)
TRY_LESS(100.0f, 50.0f + step)
TRY_GREATER(500.0f)
TRY_GREATER(250.0f)
while (true)
{
if (CalculateLuminosity(p_constant, p_linear, p_quadratic, p_intensity, distance) < threshold) // If the light has a very low luminosity for the given distance, we consider the current distance as the light radius
{
return distance;
}
else
{
distance += step;
}
}
}
得到distance后,进入无限循环,每一个循环判断光强是否小于阈值,是则将当前distance作为半径返回;反之则令distance增加一个步长step(因为上文中的if判断是向下靠近的,distance会始终<=半径),进行新一轮循环,如此得到一个可靠的点光源覆盖半径。
2.4CalculateAmbientBoxLightRadius
float CalculateAmbientBoxLightRadius(const OvMaths::FVector3& p_position, const OvMaths::FVector3& p_size)
{
return OvMaths::FVector3::Distance(p_position, p_position + p_size);
}
AmbientBoxLight是一种特殊的光源,它的半径将会由外界传入p_size决定,详细的内容马上会提到。
2.5GetEffectRange
在实现了半径的计算,我们需要根据当前的光源类型使用不同的函数,将type强制转化为整型,再转化为光源枚举类型,当type为点光源或聚光时,调用CalculatePointLightRadius,传入衰减的3个系数与基础光强;
当type为AMBIENT_BOX时,调用CalculateAmbientBoxLightRadius,它将衰减的3个系数作为偏移量,获得一个新的向量newPosition,并与position做点乘得到newPosition在position上的投影,将投影值作为半径;
当type为AMBIENT_SPHERE时,则半径将会是一个常数,即衰减系数中的常数项;
若是其他情况,即平行光等,则半径为无穷大。
float OvRendering::Entities::Light::GetEffectRange() const
{
switch (static_cast<OvRendering::Entities::Light::Type>(static_cast<int>(type)))
{
case Type::POINT:
case Type::SPOT: return CalculatePointLightRadius(constant, linear, quadratic, intensity);
case Type::AMBIENT_BOX: return CalculateAmbientBoxLightRadius(m_transform.GetWorldPosition(), { constant, linear, quadratic });
case Type::AMBIENT_SPHERE: return constant;
}
return std::numeric_limits<float>::infinity();
}
以上就是有关light的全部内容。
(博客内容参考LearnOpenGL)