从0开始的OpenGL学习(十四)-3种光源模型

本文主要解决一个问题:

如何在OpenGL中模拟三种光源类型?

引言

之前的文章中,我们把光源定义成空间中的一点。效果确实不错,但是还不足以模拟现实世界中的大部分光源。一个简单的例子,它无法模拟太阳光。在本章中,我们会介绍3中模拟真实世界中光源的模型,使用这三种模型我们可以模拟绝大部分的光源。这三种光源模型是:方向光、点光源、聚光灯。

我们先从方向光开始,然后是点光源,最后是聚光灯。

方向光(Directional Light)

方向光模型模拟的是一个非常远的地方发射出来的光。在非常远的距离上,到物体上就近似于平行。想想太阳光,太阳距离地球大约1.5亿公里,地球的半径是6378公里,算起来,太阳光照射的角度范围大约只有0.0024度,照到地球的时候和平行也没什么区别了。

方向光模型

由于光线都是平行照射,光照效果也就和光源位置无关。所以,方向光的模型需要的是一个方向参数而不是位置,着色器在计算的时候也几乎相同。我们来改一下光源结构:

struct Light{
    vec3 direction;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
void main(){
...
    vec3 lightDir = normalize(-light.direction);  
...
}

注意我们要光线方向的反方向用于计算角度。那为啥不直接指定反方向呢?这是一个习惯的问题,说到方向光,我们最直接的反应就是光线方向,这最符合我们的逻辑认识。

要观察方向光的效果,我们需要在之前显示3D盒子章节中的10个盒子。回忆一下之前的章节,首先我们要需要10个不同的位置:

glm::vec3 cubePositions[] = {
  glm::vec3( 0.0f,  0.0f,  0.0f), 
  glm::vec3( 2.0f,  5.0f, -15.0f), 
  glm::vec3(-1.5f, -2.2f, -2.5f),  
  glm::vec3(-3.8f, -2.0f, -12.3f),  
  glm::vec3( 2.4f, -0.4f, -3.5f),  
  glm::vec3(-1.7f,  3.0f, -7.5f),  
  glm::vec3( 1.3f, -2.0f, -2.5f),  
  glm::vec3( 1.5f,  2.0f, -2.5f), 
  glm::vec3( 1.5f,  0.2f, -1.5f), 
  glm::vec3(-1.3f,  1.0f, -1.5f)  
};

还需要10个把模型从局部空间转换到世界空间的模型矩阵:

for(unsigned int i = 0; i < 10; i++)
{
    glm::mat4 model;
    model = glm::translate(model, cubePositions[i]);
    float angle = 20.0f * i;
    model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
    lightingShader.setMat4("model", model);

    glDrawArrays(GL_TRIANGLES, 0, 36);
}

最后,别忘了设置方向光,你可以在主循环外面,也可以在主循环里面设置。当然,之前对光源位置的引用也都需要删除。

lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f); 

编译运行,如果程序没有问题,你看到的场景应该是类似这样的:

运行效果

看上去像是从天上有光照下来把这些箱子照亮了。如果你显示的不对,完整的源代码在这里。

点光源(Point Light)

方向光通常用来模拟整个场景接收的全局光照,但是除了全局光照之外,我们通常还需要一些小的光源,例如一个灯泡之类的。这些光源就是点光源。点光源常常被设置在某个位置上,然后随着离距离的变远光照强度变小。

点光源效果

之前的章节里,我们用到了最简单的点光源。这个点光源有个缺点,就是光照不会随着距离减弱,反而好像是越来越强了,这显然是不符合常理的。在大多数3D场景中,我们希望的点光源是像现实生活中那样,只能照亮周围一小片区域。

如果实现过之前章节中的10个盒子,你可能会注意到盒子背面的亮度和前面的亮度是一致的,因为我们没有对光照的强度进行衰减。你是对的!光照应该是随着距离越远越弱。

衰减(Attenuation)

随着距离的变远光照强度减弱的过程我们称之为衰减。一个简单的方法是直接采用线性衰减:设定一个衰减比例,随着距离减少强度。但是这种衰减不符合现实的情况,现实情况是光照会在短距离之内迅速衰减,然后缓慢衰减直至消失。没错,这更像是一种二次衰减模型。

幸运的是,有一些聪明的前辈高人已经将这个衰减公式给计算出来了。我们直接就能使用:

衰减公式

这里的d表示距离(distance),是片元到光源的距离。公式中包含了3个常数因子,分别是Kc, Kl和Kq。这三个因子分别是常数衰减指数、线性衰减指数、二次项衰减指数。

因为二次项的衰减会比前面的线性和常数衰减快很多,造成的结果就是在离光源近的地方会很亮,然后离开光源,亮度迅速衰减,到一定程度后衰减又会减慢。整个过程看起来就是像是这个样子:

衰减图

3个衰减因子到底应该选多少呢?

衰减因子取决于很多因素:环境、你期望光源覆盖的范围、光的类型等等。大多数情况下,这是一个经验和微调的问题。下面一张表里给出了覆盖范围和衰减因子的取值关系,这些值是非常好的微调基准值。

范围和衰减因子关系表

就像你看到的这样,Kc永远是1,Kl随着覆盖范围增大变得非常小,而Kq变小的就更快了。有时间试试这些值,对渲染场景的影响。在本文中,我们选择覆盖范围是50。

实现点光源效果。光源的位置属性,再往Light结构中添加三个float变量表示3个不同的衰减因子,这些因子可以通过主函数设置。

struct Light{
    //vec3 direction;
    vec3 position;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;

    float constant;
    float linear;
    float quadratic;
};

然后我们就可以在主函数中设置这些值了。对照上面的表,我们的3个衰减因子分别设置为:1.0f, 0.09f, 0.032f。

lightingShader.setFloat("light.constant", 1.0f);
lightingShader.setFloat("light.linear", 0.09f);
lightingShader.setFloat("light.quadratic", 0.032f);

将衰减值应用到光照中去也非常简单,只要计算出衰减值,然后乘上ambient,diffuse和specular分量就行了。

先计算衰减值。我们要用到片元距离光源的距离,这就要用到GLSL内置的length函数了,这个函数作用是计算一个向量的长度,我们把光源位置和片元位置做减法就可以得到这个向量。

float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
显示效果

如果你显示的结果不对,请下载源码进行对比。

聚光灯(Spot Light)

最后一种常见的光源类型是聚光灯。聚光灯模型,模拟的是手电筒,探照灯之类可以把光汇聚到一个方向的光源。它用到了平行光和点光源的部分内容,我们在设置聚光灯的时候,需要设置其位置和朝向,并且光照强度会随着距离而减小。特别的地方是,聚光灯的光只会对某个方向上的有限圆锥角的物体有照亮效果,如下图所示:

聚光灯
  • LightDir(光照方向):表示光源到片元的方向
  • SpotDir(聚光灯朝向):表示聚光灯前方的方向,也就是影响方向。
  • Phi ϕ:范围角度,所有在这个角度范围之外的物体都不会被照亮
  • Theta θ:光照方向和聚光灯朝向的夹角,用来计算光照强度
实现一个手电筒

聚光灯需要位置、朝向和范围角度,因此,我们要在光源结构体中添加这些成员。

struct Light {
    vec3  position;
    vec3  direction;
    float cutOff;
    ...
};  

设置这些值:

lightingShader.setVec3("light.position",camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
lightingShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));

可以看到,我们是用cos角度值来代替原本的角度,因为这样比较简单。我们可以直接计算光照方向和聚光灯方向的点积,然后和这个数值进行比较从而得出该点是否接收光照这个结论。

现在,我们在片元着色器里计算片元和光源之间的方向与聚光灯朝向之间的夹角是否超过了照射范围:

float theta = dot(lightDir, normalize(-light.direction));
if (theta > light.cutOff) { //在照射范围内
}
else
        FragColor = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);

注意,cos的值随着角度变大而逐渐变小,所以判断的方式是theta>light.cutOff表示在照射范围内。

编译运行,你会看到类似这样的效果:

运行效果

如果不对,请下载源码进行比对。

看上去有点假,有没有这感觉?

平滑边缘

为了创建一个平滑的边缘效果,我们需要改变一下聚光灯的模型,模拟聚光灯的内锥角和外锥角。计算方式也会有所改变。

假设内锥角为ϕ,外锥角为γ, 光照角度为θ,我们的光照强度的计算公式就是:

计算公式

其中:ϵ为(内锥角-外锥角)的cos值。结果I 就表示当前片元的光照强度。

让我们来看计算代码:

//在光源结构体中添加外锥角成员
struct Light{
...
float outerCutOff;
};

//聚光灯
float theta = dot(lightDir, normalize(-light.direction));   //计算片元角度的cos值
float epsilon = light.cutOff - light.outerCutOff;   //计算epsilon的值,用内锥角的cos值减去外锥角的cos值
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);   //根据公式计算光照强度,并限制结果的范围

diffuse *= intensity;
specular *= instensity;

强度被限制在0到1之间,这是必要的,因为theta-light.outerCutOff的值可能是负数。

最后,设置内锥角度为12.5度,外锥角度为17.5度,设置代码如下:

lightingShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
lightingShader.setFloat("light.outerCutOff", glm::cos(glm::radians(17.5f)));

编译运行,如果运行没问题,你所看到的结果应该是这个样子:

运行结果

如果你的显示不正确,欢迎参考源码

总结

本章中,我们学习了3中光照模型,分别是:方向光,点光源和聚光灯。方向光最简单,只有一个方向参数。点光源稍微复杂点,有位置和衰减度两个参数。最复杂的是聚光灯,不仅有方向、衰减度,还有内锥角和外锥角的区分。不过,功夫不负有心人,我们终于弄出点有趣的效果来了。

下一篇
目录
上一篇

参考资料

www.learnopengl.com(非常好的网站,欢迎学习)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容