教程 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的四边形平面,法线是一个垂直的向量。

【一步步学OpenGL 20】 -《点光源》的更多相关文章

  1. 【一步步学OpenGL 21】 -《聚光灯光源》

    教程 21 聚光灯光源 原文: http://ogldev.atspace.co.uk/www/tutorial21/tutorial21.html CSDN完整版专栏: http://blog.cs ...

  2. 步步学LINQ to SQL:为实体类添加关系【转】

    [IT168 专稿]本文详细为你阐述了如何在你的应用程序中实现LINQ to SQL.附件的示例程序包括了这里探讨的所有代码,还提供了一个简单的WPF图形界面程序来显示通过数据绑定返回的结果集. 第一 ...

  3. 步步学LINQ to SQL:使用LINQ检索数据【转】

    [IT168 专稿]该系列教程描述了如何采用手动的方式映射你的对象类到数据表(而不是使用象SqlMetal这样的自动化工具)以便能够支持数据表之间的M:M关系和使用实体类的数据绑定.即使你选择使用了自 ...

  4. 步步学LINQ to SQL:将类映射到数据库表【转】

    [IT168 专稿]该系列教程描述了如何采用手动的方式映射你的对象类到数据表(而不是使用象SqlMetal这样的自动化工具)以便能够支持数据表之间的M:M关系和使用实体类的数据绑定.即使你选择使用了自 ...

  5. 1113: 零起点学算法20——输出特殊值II

    1113: 零起点学算法20--输出特殊值II Time Limit: 1 Sec  Memory Limit: 64 MB   64bit IO Format: %lldSubmitted: 207 ...

  6. 重学OpenGL(一)----工具篇

    最近想开发一个小工具,需要用到3D,果断上OpenGL,借这个过程把OpenGL重学一遍. 工欲善其事,必先利其器,先把工具都搞好. [开发语言] 果断C+OpenGL,不解释. [开发环境] Min ...

  7. 一步步学Mybatis-搭建最简单的开发环境-开篇(1)

    最近抽空学习了Mybatis这个框架,在学习的过程中也找了很多的文章,个人感觉官网上的东西太多太杂,不适合许多希望一步步快速上手的朋友们,当然觉得查阅问题的时候可以直接通过官网找还比较快或者是Stac ...

  8. 学OpenGL的一些好的网站

    好的资源太多,自己懂的太少,而今迈步从头越!!fighting...... 一些OpenGL资源链接 这是前几天自己简单整理的几个链接,希望对大家有用 顺便问一下http://www.spacesim ...

  9. 一步步学算法(算法分析)---6(Floyd算法)

    Floyd算法 Floyd算法又称为弗洛伊德算法,插点法,是一种用于寻找给定的加权图中顶点间最短路径的算法.该算法名称以创始人之一.1978年图灵奖获得者.斯坦福大学计算机科学系教授罗伯特·弗洛伊德命 ...

随机推荐

  1. eclipse里面svn比较之前版本的代码

    team——显示资源历史记录比较

  2. mumu模拟器设置代理/打开网络连接(windows)

    adb_server.exe devicesadb_server.exe connect 127.0.0.1:7555adb_server.exe shell am start -a android. ...

  3. vue cli3.0 结合echarts3.0和地图的使用方法

    echarts 提供了直观,交互丰富,可高度个性化定制的数据可视化图表.而vue更合适操纵数据. 最近一直忙着搬家,就没有更新博客,今天抽出空来写一篇关于vue和echarts的博客.下面是结合地图的 ...

  4. C# ImageHelper

    using System; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Web; ...

  5. 南阳171----聪明的kk

    //简单的dp #include<cstdio> #define Max(a,b) ((a)>(b)?(a):(b)) ]; int main() { int i,j,n,m,x,t ...

  6. 洛谷P3375 [模板]KMP字符串匹配

    To 洛谷.3375 KMP字符串匹配 题目描述 如题,给出两个字符串s1和s2,其中s2为s1的子串,求出s2在s1中所有出现的位置. 为了减少骗分的情况,接下来还要输出子串的前缀数组next.如果 ...

  7. 2017-9-17-MDIO信号线串联小电阻作用【转】

    今天做集成测试的时候被领导说测到的MDIO信号过冲较大(正反向过冲都很大),容易损坏接口或阻容,万一那个电容耐压值不够就挂了. 我原本是不屑的,私以为MDIO.IIC.SPI等只要抓到的波形不影响判决 ...

  8. [ZJOI2016]大森林

    Description: 小Y家里有一个大森林,里面有n棵树,编号从1到n 0 l r 表示将第 l 棵树到第 r 棵树的生长节点下面长出一个子节点,子节点的标号为上一个 0 号操作叶子标号加 1(例 ...

  9. [P1396]营救 (并查集)

    大佬都是用最短路做的 我用最小生成树 #include<bits/stdc++.h> #include<algorithm> using namespace std; stru ...

  10. 【迎圣诞,拿大奖】+流量分析+Writeup分享

    太菜了太菜了,刚见到jsfuck时竟然不知道什么东西,自己都不敢说自己做过实验吧上的那道jsfuck题了. 进入正题: 首先解压发现两个文件,一个流量分析包,哇哇哇,我正好刚学了几天wireshark ...