【一步步学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中的核心工 ...
随机推荐
- PCL特征点与配准(1)
关于输入一个具体的物体的点云,从场景中找出与该物体点云相匹配的,这种方法可以用来抓取指定的物体等等,具体的代码的解释如下,需要用到的一些基础的知识,在之前的博客中都有提及,其中用到的一些方法可以翻阅前 ...
- 深入浅出学习Hibernate框架(二):JDBC基础操作
上篇博客<深入浅出学习Hibernate框架(一):从实例入手初识Hibernate框架>简单介绍了一下Hibernate框架,并且举了一个实例来了解Hibernate.这篇博客将介绍JD ...
- 磁盘映射: between 宿主机 and 客户机
一.虚拟机映射到宿主机 在虚拟机关机的状态下,双击右侧设备栏里硬盘,在弹出的窗口中单击“实用程序“,选择“映射”.打开映射虚拟磁盘的窗口,其中的“卷”就是你希望映射虚拟机中的哪个分区到主机,如 ...
- Selenium常用操作汇总二——如何把一个元素拖放到另一个元素里面(转)
Q群里有时候会有人问,selenium webdriver怎么实现把一个元素拖放到另一个元素里面.这一节总一下元素的拖放. 下面这个页面是一个演示拖放元素的页面,你可以把左右页面中的条目拖放到右边的 ...
- FaceAlignment blog
https://blog.csdn.net/app_12062011/article/category/7574425 https://www.jianshu.com/p/e4b9317a817f
- Pandas DataFrame 函数应用和映射
apply Numpy 的ufuncs通用函数(元素级数组方法)也可用于操作pandas对象: 另一个常见的操作是,将函数应用到由各列或行所形成的一维数组上.Dataframe的apply方法即可实现 ...
- Java Web知识梳理
今天给内部做了个培训,貌似搞错了对象,不该对新人讲这么原理性的东西. anyway,还是放上来吧,不知道有没有人能理清其中的逻辑 ^ _ ^ 问题:为什么要用tomcattomcat: servlet ...
- 关于阅读JDK源码的准备
说明:本篇是给自己看的. 笑 最近突然有冲动 想研究下JDK的源码,搜索了一番,基本上推荐从集合开始,精华部分包括:集合.IO.多线程.网络编程. 虚拟机部分先放一放吧,感觉现在不适合我这种半路出家的 ...
- 第三百六十四节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)的mapping映射管理
第三百六十四节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)的mapping映射管理 1.映射(mapping)介绍 映射:创建索引的时候,可以预先定义字 ...
- SpringMVC系列(九)自定义视图、重定向、转发
一.自定义视图 1. 自定义一个视图HelloView.java,使用@Component注解交给Spring IOC容器处理 package com.study.springmvc.views; i ...