【一步步学OpenGL 20】 -《点光源》
教程 20
点光源
原文: http://ogldev.atspace.co.uk/www/tutorial20/tutorial20.html
CSDN完整版专栏: http://blog.csdn.net/column/details/13062.html
背景
之前已经学习了三个主要的光照模型(环境光,漫射光和镜面反射光),这三种模型都是基于平行光的。平行光仅仅是通过一个向量来表示,没有光源起点,因此它不会随着距离的增大而衰减(实际上没有起点根本无法定义光源和某个物体的距离)。
如今我们再来看点光源类型,它有光源起点并且有衰减效果。距离光源越远光线越弱。点光源的经典样例是灯泡。灯泡在屋子里可能效果不明显,可是拿到室外就会明显看出它的衰减效果了。注意之前平行光的方向是恒定的。但点光源光线的方向是变化的,四处扩散。
点光源想各个方向均匀照耀,因此点光源的方向要通过计算物体到点光源之间的向量得到,这就是为什么要定义点光源的起点而不是它的方向。
点光源光线慢慢变淡的的想象叫做‘衰减’。真实光线的衰减是依照平方反比定律的。也就是说光线的强度和离光源的距离的平方成反比。数学原理例如以下图中的公式:
但3D图形中这个公式计算的结果看上去效果并不好。比如:当距离非常近时,光的强度接近无穷大了。
另外,开发人员除了通过设置光的起始强度外无法控制点光源的亮度,这样就太受限制了。因此我们加入了几个新的因素到公式中使对其的控制更加灵活:
我们在分母上加入了三个光衰减的參数因子。一个常量參数,一个线性參数和一个指数參数。当将常量參数和线性參数设置为零且指数參数设置为1时,就和实际的物理公式是相应的了。也就是这个特殊情况下在物理上是准确的。
当设置常量因子參数为1时。调节另外两个參数总体上就有比較好的衰减变化效果了。
常量參数的设置是要保证当距离为0时光照强度达到最大(这个要在程序内进行配置),然后随着距离的增大光照强度要慢慢减弱,因分母在慢慢变大。控制好线性參数因子和指数參数因子的变化,就能够实现想要的衰减效果。线性參数主要用于实现缓慢的衰减效果而指数因子能够控制光强度的迅速衰减。
如今总结计算点光源须要的步骤:
- 计算和平行光一样的环境光;
- 计算一个从像素点(世界空间中的)到点光源的向量作为光线的方向。利用这个光线方向就能够计算和平行光一样的漫射光以及镜面反射光了。
- 计算像素点到点光源的距离用来计算终于的光线强度衰减值;
- 将三种光叠加在一起。计算得到终于的点光源颜色,通过点光源的衰减性三种光看上去也能够被分离开了。
源码具体解释
(lighting_technique.h:24)
struct BaseLight
{
Vector3f Color;
float AmbientIntensity;
float DiffuseIntensity;
};
.
.
.
struct PointLight : public BaseLight
{
Vector3f Position;
struct
{
float Constant;
float Linear;
float Exp;
} Attenuation;
}
平行光尽管和点光源不一样。但它们仍然有非常多共同之处。它们共同的部分都放到了BaseLight结构体中,而点光源和平行光的结构体则继承自BaseLight。平行光额外加入了方向属性到它的类中,而点光源则加入了世界坐标系中的位置变量和那三个衰减參数因子。
(lighting_technique.h:81)
void SetPointLights(unsigned int NumLights, const PointLight* pLights);
这个教程除了展示如何实现点光源。还展示如何使用多光源。通常仅仅存在一个平行光光源,也就是太阳光,另外可能还会有一些点光源(屋子里的灯泡。地牢里的火把等等)。
这个函数參数有一个点光源数据结构的数组和数组的长度。使用结构体的值来更新shader。
(lighting_technique.h:103)
struct {
GLuint Color;
GLuint AmbientIntensity;
GLuint DiffuseIntensity;
GLuint Position;
struct
{
GLuint Constant;
GLuint Linear;
GLuint Exp;
} Atten;
} m_pointLightsLocation[MAX_POINT_LIGHTS];
为了支持多个点光源,shader须要包括一个和点光源结构体(仅仅在GLSL中)内容一样的结构体数组。主要有两种方法来更新shader中的结构体数组:
能够获取每一个数组元素中每一个结构字段的位置(比如,一个数组假设有五个结构体,每一个结构体四个字段,那就须要20个‘位置一致变量’),然后单独设置每一个元素中每一个字段的值。
也能够仅仅获取数组第一个元素每一个字段的位置,然后用一个GL函数来保存元素中每一个字段的属性类型。比如,数组元素也就是一个结构体的第一个字段是一个float变量。第二个是一个integer变量。就能够在一次回调中使用一个float数组遍历设置数组中每一个结构体第一个字段的值,然后在第二次回调中使用一个int数组来设置每一个结构体的第二个值。
第一种方法由于要维护大量的位置一致变量因此非常浪费资源。可是会更加灵活。由于你能够通过位置一致变量訪问更新数组中的不论什么一个元素,不须要像另外一种方法那样先要转换输入的数据。
另外一种方法不须要管理那么多的位置一致变量。可是假设想要同一时候更新数组中的几个元素的话,同一时候用户传入的又是一个结果体数组(像SetPointLights()),你就要先将这个结构体数组转换成多个字段的数组结构,由于结构体中每一个位置的字段数据都要使用一个同类型的数组来更新。当使用结构体数组时,在数组中两个连续元素(结构体)中的同一个字段之间存在内存间隔(被其它字段间隔开了,我们是想要同一个字段的连续字段数组)。须要将它们收集到它们自己的同类型数组中。本教程中,我们将使用第一种方法。最好两个都实现一下,看你认为哪一个方法更好用。
MAX_POINT_LIGHTS是一个常量,用于限制能够使用的点光源的最大数量,并且必须和着色器中的相应值同步一致。默认值为2。当你添加应用中光的数量,随着光源的添加会发现性能越来越差。这个问题能够使用一种称为“延迟着色”的技术来优化解决,这个后面再探讨。
(lighting.fs:46)
vec4 CalcLightInternal(BaseLight Light, vec3 LightDirection, vec3 Normal)
{
vec4 AmbientColor = vec4(Light.Color, 1.0f) * Light.AmbientIntensity;
float DiffuseFactor = dot(Normal, -LightDirection);
vec4 DiffuseColor = vec4(0, 0, 0, 0);
vec4 SpecularColor = vec4(0, 0, 0, 0);
if (DiffuseFactor > 0) {
DiffuseColor = vec4(Light.Color * Light.DiffuseIntensity * DiffuseFactor, 1.0f);
vec3 VertexToEye = normalize(gEyeWorldPos - WorldPos0);
vec3 LightReflect = normalize(reflect(LightDirection, Normal));
float SpecularFactor = dot(VertexToEye, LightReflect);
if (SpecularFactor > 0) {
SpecularFactor = pow(SpecularFactor, gSpecularPower);
SpecularColor = vec4(Light.Color * gMatSpecularIntensity * SpecularFactor, 1.0f);
}
}
return (AmbientColor + DiffuseColor + SpecularColor);
}
这里在平行光和点光源之间实现非常多着色器代码的共享就不算什么新技术了。大多数算法是同样的。不同的是,我们仅仅须要考虑点光源的衰减因素。 此外,针对平行光,光的方向是由应用提供的。而对点光源,须要计算每一个像素的光的方向。
上面的函数封装了两种光类型之间的共用部分。 BaseLight结构体包括光强度和颜色。
LightDirection是额外单独提供的,原因上面刚刚已经提到。 另外还提供了顶点法线,由于我们在进入片段着色器时要对其进行一次单位化处理。然后在每次调用此函数时使用它。
(lighting.fs:70)
vec4 CalcDirectionalLight(vec3 Normal)
{
return CalcLightInternal(gDirectionalLight.Base, gDirectionalLight.Direction, Normal);
}
有了公共的封装函数,定义函数简单的包装调用一下就能够计算出平行光了,參数多数来自全局变量。
(lighting.fs:75)
vec4 CalcPointLight(int Index, vec3 Normal)
{
vec3 LightDirection = WorldPos0 - gPointLights[Index].Position;
float Distance = length(LightDirection);
LightDirection = normalize(LightDirection);
vec4 Color = CalcLightInternal(gPointLights[Index].Base, LightDirection, Normal);
float Attenuation = gPointLights[Index].Atten.Constant +
gPointLights[Index].Atten.Linear * Distance +
gPointLights[Index].Atten.Exp * Distance * Distance;
return Color / Attenuation;
}
计算点光比定向光要复杂一点。每一个点光源的配置都要调用这个函数。因此它将光的索引作为參数,在全局点光源数组中找到相应的点光源。
它依据光源位置(由应用程序在世界空间中提供)和由顶点着色器传递过来的顶点世界空间位置来计算光源方向向量。使用内置函数length()计算从点光源到每一个像素的距离。 一旦我们有了这个距离。就能够对光的方向向量进行单位化处理。
注意,CalcLightInternal()是须要一个单位化的光方向向量的。平行光的单位化由LightingTechnique类来负责。 我们使用CalcInternalLight()函数获得颜色值。并使用我们之前得到的距离来计算光的衰减。终于点光源的颜色是通过将颜色和衰减值相除计算得到的。
(lighting.fs:89)
void main()
{
vec3 Normal = normalize(Normal0);
vec4 TotalLight = CalcDirectionalLight(Normal);
for (int i = 0 ; i < gNumPointLights ; i++) {
TotalLight += CalcPointLight(i, Normal);
}
FragColor = texture2D(gSampler, TexCoord0.xy) * TotalLight;
}
有了前面的基础。片段着色器方面就变得非常easy了。简单地将顶点法线单位化。然后将全部类型光的效果叠加在一起。结果再乘以採样的颜色,就得到终于的像素颜色了。
(lighting_technique.cpp:279)
void LightingTechnique::SetPointLights(unsigned int NumLights, const PointLight* pLights)
{
glUniform1i(m_numPointLightsLocation, NumLights);
for (unsigned int i = 0 ; i < NumLights ; i++) {
glUniform3f(m_pointLightsLocation[i].Color, pLights[i].Color.x, pLights[i].Color.y, pLights[i].Color.z);
glUniform1f(m_pointLightsLocation[i].AmbientIntensity, pLights[i].AmbientIntensity);
glUniform1f(m_pointLightsLocation[i].DiffuseIntensity, pLights[i].DiffuseIntensity);
glUniform3f(m_pointLightsLocation[i].Position, pLights[i].Position.x, pLights[i].Position.y, pLights[i].Position.z);
glUniform1f(m_pointLightsLocation[i].Atten.Constant, pLights[i].Attenuation.Constant);
glUniform1f(m_pointLightsLocation[i].Atten.Linear, pLights[i].Attenuation.Linear);
glUniform1f(m_pointLightsLocation[i].Atten.Exp, pLights[i].Attenuation.Exp);
}
}
此函数通过迭代遍历数组元素并依次传递每一个元素的属性值。然后使用点光源的值更新着色器。 这是前面所说的“方法1”。
本教程的Demo显示两个点光源在一个场景区域中互相追逐。
一个光源基于余弦函数,而还有一个光源基于正弦函数。该场景区域是由两个三角形组成的非常easy的四边形平面,法线是一个垂直的向量。