原文: http://ogldev.atspace.co.uk/www/tutorial20/tutorial20.htmlcss
CSDN完整版专栏: http://blog.csdn.net/column/details/13062.htmlhtml
以前已经学习了三个主要的光照模型(环境光,漫射光和镜面反射光),这三种模型都是基于平行光的。平行光仅仅是经过一个向量来表示,没有光源起点,所以它不会随着距离的增大而衰减(实际上没有起点根本没法定义光源和某个物体的距离)。算法
现在咱们再来看点光源类型,它有光源起点而且有衰减效果。距离光源越远光线越弱。点光源的经典样例是灯泡。灯泡在屋子里可能效果不明显,但是拿到室外就会明显看出它的衰减效果了。注意以前平行光的方向是恒定的。但点光源光线的方向是变化的,四处扩散。数组
点光源想各个方向均匀照耀,所以点光源的方向要经过计算物体到点光源之间的向量获得,这就是为何要定义点光源的起点而不是它的方向。markdown
点光源光线慢慢变淡的的想象叫作‘衰减’。真实光线的衰减是依照平方反比定律的。也就是说光线的强度和离光源的距离的平方成反比。数学原理例如如下图中的公式:数据结构
但3D图形中这个公式计算的结果看上去效果并很差。好比:当距离很是近时,光的强度接近无穷大了。函数
另外,开发人员除了经过设置光的起始强度外没法控制点光源的亮度,这样就太受限制了。所以咱们加入了几个新的因素到公式中使对其的控制更加灵活:post
咱们在分母上加入了三个光衰减的參数因子。一个常量參数,一个线性參数和一个指数參数。当将常量參数和线性參数设置为零且指数參数设置为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的四边形平面,法线是一个垂直的向量。