基于平行光,我们已经学习了三种基本光照模型(环境光、漫反射光和高光)。平行光是一种只有单一方向并且没有起点的灯光类型。因此,它不会随着距离增大而减弱(事实上,你甚至不能定义它与目标的距离)。我们来定义一下点光源,点光源有起点和衰减效果,并且当物体远离点光源时这种衰减效果变得更为明显。灯泡(light blub)其实就是一个点光源,在房间里你可能感受不到灯泡发出的光照的衰减效果;但是一到了室外,你会很快看到灯光强度有限(随距离增大衰减明显)。注意,平行光在整个场景中的方向都不变,而点光源光线的方向则是不一样的。这是因为点光源向各个方向平等地发出光线,因此我们必须要通过从物体到点光源的矢量来为每个物体计算其接受的光照的方向。这就是我们为点光源定义源位置而非方向的原因。
点光源的衰退效应常被叫做“衰减”。实际物体的“衰减”遵循平方反比法则(光强与物体到光源的距离的平方成反比)。数学公式如下:
这个公式产生的 3D 视觉效果不是很好。例如,当距离变得非常小的时候,光线的强度就会趋于无穷大。另外,开发者除了可以设置光的初始强度外,并没有其他手段来控制光照的最后效果。这种做法局限性太大了。因此,我们给这个公式添加一些因子,来让它变得更灵活:
我们给分母添加了三个光衰减因子(一个常量因子,一个线性因子和一个指数因子)。当我们把常量和线性因子设置为 0,并且指数因子设置为 1 的时候,就得到了一个精确的公式(即本讲第一个公式)。你会发现把常量因子设置成 1 而把另外两个因子设置成很小的分数是很有用的。当把常量因子设置成 1,基本上就可以保证光强在距离为零处达到最大值(事实上,这个值是你在在程序中指定的),并且随着距离变大而衰减,因为分母将比 1 大。当你协调好线性和指数因子的时候,你会得到想要的随着距离急剧或缓慢衰减的灯光效果。 我们来总结一下计算点光源需要的步骤:
(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; |
} |
(lighting_technique.h:81) |
void SetPointLights(unsigned int NumLights, const PointLight* pLights); |
(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]; |
第一个方法,就你必须要得到每个一致变量的地址,导致资源比较浪费,但是用起来却比较灵活。你可以通过简单的访问地址来更新整个数组中的任意变量,而且不需要像第二个方法那样对你的输入的数据进行变换。
第二个方法对一致变量地址的管理比较少,但是如果你想一次性更新几个数组元素你的用户传递的参数是一个结构体数组(像在 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, 1.0f) * Light.DiffuseIntensity * DiffuseFactor; |
vec3 VertexToEye = normalize(gEyeWorldPos - WorldPos0); |
vec3 LightReflect = normalize(reflect(LightDirection, Normal)); |
float SpecularFactor = dot(VertexToEye, LightReflect); |
SpecularFactor = pow(SpecularFactor, gSpecularPower); |
if (SpecularFactor > 0) { |
SpecularColor = vec4(Light.Color, 1.0f) * |
gMatSpecularIntensity * SpecularFactor; |
} |
} |
return (AmbientColor + DiffuseColor + SpecularColor); |
} |
其实不需要太过吃惊于平行光和点光源之间会共享相当多的着色器代码。程序中的大部分计算过程都是一样的,而不同的是,我们只需要将点光源的衰减因子考虑进来。另外,由程序提供光照方向只是对平行光来说的,而点光源的光线方向必须由我们自己挨个为每个像素计算出来。
上面的这个函数封装了平行光和点光源之间的共同部分。BaseLight 结构体包含强度和颜色。光照方向基于上述原因需要单独提供,正是因为上面所考虑到的原因。第三个参数是顶点法线,提供它是因为进入片元着色器时我们对其进行一次标准化,然后这个函数可以多次调用它。
(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; |
} |
(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; |
} |
(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); |
} |
} |
这章的示例展示了两个点光源在一个场景中互相追逐。一个光源的轨迹沿 cos 函数,另一个沿 sin 函数。这个场景是一个由两个三角形构成的简单的四边形,其发现方向竖直向上。