【Overload游戏引擎】源码分析之十二:OvRendering函数库(十)

本文深入探讨Overload游戏引擎中OvRendering的灯光系统,包括平行光、点光源、衰减、聚光和平滑/软化边缘等概念。文章详细解析了光源类型、光的衰减计算、聚光的实现以及点光源的有效照明半径计算方法。通过对光源的理解,有助于更好地掌握游戏引擎中的光照效果实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2021SC@SDUSC

目录

1.投光物

1.1平行光

1.2点光源

1.3衰减

1.4聚光

1.5平滑/软化边缘

2.Light

2.1GenerateMatrix

2.2Pack

2.3CalculatePointLightRadius

2.4CalculateAmbientBoxLightRadius

2.5GetEffectRange


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所提供:

距离常数项一次项二次项
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甚至更小。

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.xposition.yposition.zconstant
forward.xforward.yforward.zlinear
colorquadratic
typecutoffouterCutoffintensity

我们可以看到在计算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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值