【一步步学OpenGL 21】 -《聚光灯光源》
教程 21
聚光灯光源

原文: http://ogldev.atspace.co.uk/www/tutorial21/tutorial21.html
CSDN完整版专栏: http://blog.csdn.net/column/details/13062.html
背景
聚光灯光源是眼下这里要介绍的第三种也是最后一种光源类型了,它比平行光和点光源要复杂,但聚光灯光源事实上是具有平行光和点光源核心特征的一种特殊光源。聚光灯光源也会随着距离衰减。但它不是像点光源照向四面八方的而是像平行光那样有一个聚光方向(相当于取点光源的一个锥形的一小部分),聚光灯光源呈锥形,因此有一个新的属性。就是离光源越远。照亮的圆形区域会越大(光源位于锥形体的尖端)。
聚光灯光源,顾名思义,相应于现实中的聚光灯,比如:手电筒。
在游戏中。聚光灯主要用于某些场景。比如:主角拿着手电筒在黑暗的地道里探索或者逃离监狱。
我们已经知道了创建聚光灯光源的全部技术,这里最后还要另外学一下怎样实现这个光源类型的锥形效果。例如以下图:

图中垂直指向地面的黑色尖头指的是光源方向,这里想实现让光源仅仅照亮两条红线夹角之间的区域。这里仍然能够使用点积来实现。我们能够定义光锥为光线方向L和红线之间的那个夹角(两条红线之间夹角的一半)。计算那个夹角的余弦值‘C’(点积计算得到)以及L和V夹角的余弦。当中V指的是光源到某个像素的向量,假设后者的值大于余弦值‘C’(夹角越小余弦越大),说明L和V之间的夹角偏小,该像素就位于被照亮的区域内。反之,像素位于区域外就不会被该光源照亮。
假设我们紧紧依照上面说的在照亮区域内就点亮像素,否则就不点亮。那样就会看上去非常假,由于照亮区域和未照亮区域之间的边界边缘会非常明显(没有一个自然的过渡),看上去会是一个清晰的圆形画在一个黑色区域(假设没有其它光源的话)。一个真实的聚光灯光源会从照亮区域的中心向圆形边缘慢慢衰减。这里我们能够利用上面计算得到的那些点积作为一个衰减的參数。
首先我们知道,当L和V两个向量相等重合时,点积为‘1’。可是用余弦来做衰减參数会有问题,由于聚光灯光源的夹角不能太大,否则范围太广就失去了聚光灯的效果,可是在夹角从0到一个比較小的角度范围内。cos值得变化是非常缓慢的,导致衰减不明显。比如:让聚光灯的夹角为20度,余弦值就为0.939,[0.939,1.0]这个变化范围就不好作为衰减參数了。在这个范围内进行插值的空间不足,造成的衰减程度不足以让眼睛察觉到。要想衰减效果明显这个參数范围应该是[0,1]。解决方法是将这个參数的小范围映射到[0,1]的范围。方法例如以下:

原理非常easy:计算大的范围和小的范围的比例,然后依据那个比例对小范围进行映射扩张就可以。
源码具体解释
(lighting_technique.h:68)
struct SpotLight : public PointLight
{
Vector3f Direction;
float Cutoff;
SpotLight()
{
Direction = Vector3f(0.0f, 0.0f, 0.0f);
Cutoff = 0.0f;
}
};
聚光灯光源的结构体继承自点光源的结构体。并加入了两个属性和点光源差别开:一个是光源的方向向量,还有一个是截断光源照亮范围的一个阈值。
阈值代表的是光源方向向量和光源到可照亮像素之间的最大夹角。
比这个阈值夹角大的像素是不会被该光源照亮的。这里还在LightingTechnique类中为shader加入了一个位置数组,用来获取shader中的聚光灯光源数组。
(lighting.fs:39)
struct SpotLight
{
struct PointLight Base;
vec3 Direction;
float Cutoff;
};
...
uniform int gNumSpotLights;
...
uniform SpotLight gSpotLights[MAX_SPOT_LIGHTS];
在GLSL中有一个聚光灯光源类型的相似的结构体。由于这里我们不能够在C++代码中进行继承,所以这里将一个点光源结构体对象作为一个成员对象变量,并在后面加入新的属性。
有一个不一样的地方是在C++代码中那个阈值是夹角本身。而在shader中这个阈值是那个夹角的余弦值。
shader着色器仅仅关心夹角的余弦值,因此计算一次并存储比为每个像素都又一次计算余弦值要高效得多。
这里还定义了一个聚关灯光源的数组,并使用一个叫做’gNumSpotLights’的计数器来限制同意应用去创建使用的聚光灯光源的数量。
(lighting.fs:85)
vec4 CalcPointLight(struct PointLight l, vec3 Normal)
{
vec3 LightDirection = WorldPos0 - l.Position;
float Distance = length(LightDirection);
LightDirection = normalize(LightDirection);
vec4 Color = CalcLightInternal(l.Base, LightDirection, Normal);
float Attenuation = l.Atten.Constant +
l.Atten.Linear * Distance +
l.Atten.Exp * Distance * Distance;
return Color / Attenuation;
}
点光源的函数有了轻微的修改:将一个点光源的结构体作为一个參数,而不是直接获取全局数组。这样更easy将它分享给聚光灯光源对象使用。其它的这里没有做修改。
(lighting.cpp:fs)
vec4 CalcSpotLight(struct SpotLight l, vec3 Normal)
{
vec3 LightToPixel = normalize(WorldPos0 - l.Base.Position);
float SpotFactor = dot(LightToPixel, l.Direction);
if (SpotFactor > l.Cutoff) {
vec4 Color = CalcPointLight(l.Base, Normal);
return Color * (1.0 - (1.0 - SpotFactor) * 1.0/(1.0 - l.Cutoff));
}
else {
return vec4(0,0,0,0);
}
}
这里这个函数是我们计算聚光灯光源效果的地方。首先得到光源到某个像素的向量,将向量单位化方便点积运算,然后和单位化了的光源方向向量进行点积运算得到他们之间夹角的余弦值。将得到的余弦值和光源的阈值(定义光源范围的最大夹角的余弦值)进行比較。假设余弦值比阈值小,说明夹角太大像素在照亮圆区域的外面,这样像素就不会被该光源点亮。这样那个阈值就能够将聚光灯光源的照亮范围限制在一个大的或者小的圆圈内。反之假设像素在照亮区域内,我们就先像点光源那样计算光源的基础颜色。然后将那个点积计算得到的參数’SpotFactor’放到上面的公式中,将这个參数线性插值到0到1的范围。最后和点光源颜色相乘计算得到终于的聚光灯颜色值。
(lighting.fs:122)
...
for (int i = 0 ; i < gNumSpotLights ; i++) {
TotalLight += CalcSpotLight(gSpotLights[i], Normal);
}
...
和点光源的计算模式一样我们在主函数通过循环遍历累加全部聚光灯光源的效果得到相应像素的终于颜色值。
(lighting_technique.cpp:367)
void LightingTechnique::SetSpotLights(unsigned int NumLights, const SpotLight* pLights)
{
glUniform1i(m_numSpotLightsLocation, NumLights);
for (unsigned int i = 0 ; i < NumLights ; i++) {
glUniform3f(m_spotLightsLocation[i].Color, pLights[i].Color.x, pLights[i].Color.y, pLights[i].Color.z);
glUniform1f(m_spotLightsLocation[i].AmbientIntensity, pLights[i].AmbientIntensity);
glUniform1f(m_spotLightsLocation[i].DiffuseIntensity, pLights[i].DiffuseIntensity);
glUniform3f(m_spotLightsLocation[i].Position, pLights[i].Position.x, pLights[i].Position.y, pLights[i].Position.z);
Vector3f Direction = pLights[i].Direction;
Direction.Normalize();
glUniform3f(m_spotLightsLocation[i].Direction, Direction.x, Direction.y, Direction.z);
glUniform1f(m_spotLightsLocation[i].Cutoff, cosf(ToRadian(pLights[i].Cutoff)));
glUniform1f(m_spotLightsLocation[i].Atten.Constant, pLights[i].Attenuation.Constant);
glUniform1f(m_spotLightsLocation[i].Atten.Linear, pLights[i].Attenuation.Linear);
glUniform1f(m_spotLightsLocation[i].Atten.Exp, pLights[i].Attenuation.Exp);
}
}
这个函数依据聚光灯光源的结构体数组来继续更新着色器程序,基本上和点光源中相应的这个函数一样,除了额外又加入了两个參数,光的方向单位化后也传给了shader,另外那个阈值夹角装换成它的余弦值之后也传给了shader(方便shader直接用它和点积运算的结果进行比較)。注意库函数cosf()使用的是弧度值參数,是先用ToRadian宏将角度转换成的弧度值。
【一步步学OpenGL 21】 -《聚光灯光源》的更多相关文章
- 【一步步学OpenGL 20】 -《点光源》
教程 20 点光源 原文: http://ogldev.atspace.co.uk/www/tutorial20/tutorial20.html CSDN完整版专栏: http://blog.csdn ...
- 步步学LINQ to SQL:为实体类添加关系【转】
[IT168 专稿]本文详细为你阐述了如何在你的应用程序中实现LINQ to SQL.附件的示例程序包括了这里探讨的所有代码,还提供了一个简单的WPF图形界面程序来显示通过数据绑定返回的结果集. 第一 ...
- 步步学LINQ to SQL:使用LINQ检索数据【转】
[IT168 专稿]该系列教程描述了如何采用手动的方式映射你的对象类到数据表(而不是使用象SqlMetal这样的自动化工具)以便能够支持数据表之间的M:M关系和使用实体类的数据绑定.即使你选择使用了自 ...
- 步步学LINQ to SQL:将类映射到数据库表【转】
[IT168 专稿]该系列教程描述了如何采用手动的方式映射你的对象类到数据表(而不是使用象SqlMetal这样的自动化工具)以便能够支持数据表之间的M:M关系和使用实体类的数据绑定.即使你选择使用了自 ...
- 重学OpenGL(一)----工具篇
最近想开发一个小工具,需要用到3D,果断上OpenGL,借这个过程把OpenGL重学一遍. 工欲善其事,必先利其器,先把工具都搞好. [开发语言] 果断C+OpenGL,不解释. [开发环境] Min ...
- 一步步学Mybatis-搭建最简单的开发环境-开篇(1)
最近抽空学习了Mybatis这个框架,在学习的过程中也找了很多的文章,个人感觉官网上的东西太多太杂,不适合许多希望一步步快速上手的朋友们,当然觉得查阅问题的时候可以直接通过官网找还比较快或者是Stac ...
- 学OpenGL的一些好的网站
好的资源太多,自己懂的太少,而今迈步从头越!!fighting...... 一些OpenGL资源链接 这是前几天自己简单整理的几个链接,希望对大家有用 顺便问一下http://www.spacesim ...
- Rhythmk 一步一步学 JAVA (21) JAVA 多线程
1.JAVA多线程简单示例 1.1 .Thread 集成接口 Runnable 1.2 .线程状态,可以通过 Thread.getState()获取线程状态: New (新创建) Runnable ...
- 一步步学敏捷开发:6、Scrum的3种工件
Scrum的3种工件包括:Product Blacklog.Sprint Backlog.完成标准. 1.产品待办事项列表(Product Backlog) 产品Blacklog是Scrum中的核心工 ...
随机推荐
- 十大要避免的Ext JS开发方法
原文地址:http://www.sencha.com/blog/top-10-ext-js-development-practices-to-avoid/ 作者:Sean LanktreeSean i ...
- python 核心编程 01
特殊变量 python用下划线作为变量的前缀和后缀指定特殊变量._XXX : 不用 'from module import *' 导入, 可以认为是模块中的私有变量__XXX__ : 系统定义的名字_ ...
- druid的配置
//----------------------------pom.xml-------------------------------- <!-- druid --> <depen ...
- Visual Studio Code自动识别编码
将设置中的"files.autoGuessEncoding"项的值改为true即可. 详情请转知乎:https://www.zhihu.com/question/34415763
- Shiro Demo
http://www.sojson.com/shiro http://blog.csdn.net/swingpyzf/article/details/46342023/
- Python中的base64模块
本文介绍Python 2.7中的base64模块,该模块提供了基于rfc3548的Base16, 32, 64编解码的接口.官方文档,参考这里. 该模块提供两套接口,传统接口基于rfc1521的Bas ...
- Winform控件学习笔记【第二天】——常用控件
背景:期末考试刚过就感冒了,嗓子火辣辣的,好难受.但是一想起要学习总结就打起精神来了,Winform控件网上也没有多少使用教程,大部分都是自己在网上零零散散的学的,大部分用的熟了,不总结会很容易忘得. ...
- SSH实现双向认证
SSH实现双向认证 由于经常需要使用scp在两台机器间拷贝文件,每次都输入密码太麻烦,于是按下面的步骤配置了一下,再使用ssh或scp登录远程机器时就不需输入密码了: A主机:192.168.100. ...
- VIM选择文本块/复制/粘贴
在正常模式下(按ESC进入)按键v进入可视化模式,然后按键盘左右键或h,l键即可实现文本的选择.其它相关命令:v:按字符选择.经常使用的模式,所以亲自尝试一下它. V:按行选择.这在你想拷贝或者移动很 ...
- Github 提交本地代 到远程 菜鸟 简明教程
方法1: 1. 右击项目文件夹 Git Bash Here 或 Git clone Url 远程仓库地址 Directory 本地文件夹 2. 拷贝(新建)项目文件 3. 添加项目文件 ...