光照-06.多光源(Multiple lights)

我们在前面的教程中已经学习了许多关于OpenGL 光照的知识,其中包括冯氏照明模型(Phong shading)、光照材质(Materials)、光照图(Lighting maps)以及各种投光物(Light casters)。本教程将结合上述所学的知识,创建一个包含六个光源的场景。我们将模拟一个类似阳光的平行光(Directional light)和4个定点光(Point lights)以及一个手电筒(Flashlight).

要在场景中使用多光源我们需要封装一些GLSL函数用来计算光照。如果我们对每个光源都去写一遍光照计算的代码,这将是一件令人恶心的事情,并且这些放在main函数中的代码将难以理解,所以我们将一些操作封装为函数。

GLSL中的函数与C语言的非常相似,它需要一个函数名、一个返回值类型。并且在调用前必须提前声明。接下来我们将为下面的每一种光照来写一个函数。

当我们在场景中使用多个光源时一般使用以下途径:创建一个代表输出颜色的向量。每一个光源都对输出颜色贡献一些颜色。因此,场景中的每个光源将进行独立运算,并且运算结果都对最终的输出颜色有一定影响。

下面是使用这种方式进行多光源运算的一般结构:

out vec4 color;

void main()
{
  // 定义输出颜色
  vec3 output;
  // 将平行光的运算结果颜色添加到输出颜色
  output += someFunctionToCalculateDirectionalLight();
  // 同样,将定点光的运算结果颜色添加到输出颜色
  for(int i = 0; i < nr_of_point_lights; i++)
    output += someFunctionToCalculatePointLight();
  // 添加其他光源的计算结果颜色(如投射光)
  output += someFunctionToCalculateSpotLight();

  color = vec4(output, 1.0);
}  

即使对每一种光源的运算实现不同,但此算法的结构一般是与上述出入不大的。我们将定义几个用于计算各个光源的函数,并将这些函数的结算结果(返回颜色)添加到输出颜色向量中。例如,两个光源靠近一个片段,则它们的综合作用下片段将会比只有一个光源靠近的情况下明亮。

平行光(Directional light)

我们要在片段着色器中定义一个函数用来计算平行光在对应的照射点上的光照颜色,这个函数需要几个参数并返回一个计算平行光照结果的颜色。

首先我们需要设置一系列用于表示平行光的变量,我们可以将这些变量定义在一个叫做DirLight的结构体中,并定义一个这个结构体类型的uniform变量。这些需要设置的变量与上节中设置的相似。

struct DirLIght {
    vec3 direction;
    
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
uniform DIrLIght dirLight;

之后我们可以将dirLight这个uniform变量作为下面这个函数原型的参数。

vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);

和C/C++一样,我们调用一个函数的前提是这个函数在调用前已经被声明过(此例中我们是在main函数中调用)。通常情况下我们都将函数定义在main函数之后,为了能在main函数中调用这些函数,我们就必须在main函数之前声明这些函数的原型,这就和我们写C语言是一样的。

你已经知道,这个函数需要一个DirLight和两个其他的向量作为参数来计算光照。如果你看过之前的教程的话,你会觉得下面的函数定义得一点也不意外:

vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
    vec3 lightDir = normalize(-light.direction);
    // Diffuse shading
    float diff = max(dot(normal, lightDir), 0.0);
    // Specular shading
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    // Combine results
    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));   
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
    return (ambient + diffuse + specular);
}

我们从之前的教程中复制了代码,并用两个向量来作为函数参数来计算出平行光的光照颜色向量,该结果是一个由该平行光的环境反射、漫反射和镜面反射的各个分量组成的一个向量。

定点光(Point light)

和计算平行光一样,我们同样需要定义一个函数用于计算定点光照。同样的,我们定义一个包含定点光源所需属性的结构体:

struct PointLight {
    vec3 position;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;

    float constant;
    float linear;
    float quadratic;
};
#define NR_POINT_LIGHTS 4
uniform PointLight pointLights[NR_POINT_LIGHTS];

现在我们有了4个PointLight结构体对象了。

我们同样可以简单粗暴地定义一个大号的结构体(而不是为每一种类型的光源定义一个结构体),它包含所有类型光源所需要属性变量。并且将这个结构体应用与所有的光照计算函数,在各个光照结算时忽略不需要的属性变量。然而,就我个人来说更喜欢分开定义,这样可以省下一些内存,因为定义一个大号的光源结构体在计算过程中会有用不到的变量。

vec3 CalcPointLight (PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
    vec3 lightDir = normalize(light.position - fragPos);
    // Diffuse shading
    float diff = max(dot(normal, lightDir), 0.0);
    // Specular shading
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(vewDir, reflectDir), 0.0), material.shininess);
    // Attenuation
    float distance = length(light.position - fragPos);
    float attenuation  = 1..0f / (light.constant + light.linear * distance + light.quadratic * distance * distance)
    // Combine results
    vec3 ambinet = light.ambinet * vec3(texture(material.diffuse, TexCoords));
    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
    ambient *= attenuation;
    diffuse *= attenuation;
    specular *= attenuation;
    return (ambient + diffuse + specular);
}

有了这个函数我们就可以在main函数中调用它来代替写很多个计算点光源的代码了。通过循环调用此函数就能实现同样的效果,当然代码更简洁。

把它们放到一起

我们现在定义了用于计算平行光和定点光的函数,现在我们把这些代码放到一起,写入文章开始的一般结构中:

void main()
{
    // 一些属性
    vec3 norm = normalize(Normal);
    vec3 viewDir = normalize(viewPos - FragPos);

    // 第一步,计算平行光照
    vec3 result = CalcDirLight(dirLight, norm, viewDir);
    // 第二步,计算顶点光照
    for(int i = 0; i < NR_POINT_LIGHTS; i++)
        result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);
    // 第三部,计算 Spot light
    //result += CalcSpotLight(spotLight, norm, FragPos, viewDir);

    color = vec4(result, 1.0);
}

每一个光源的运算结果都添加到了输出颜色上,输出颜色包含了此场景中的所有光源的影响。如果你想实现手电筒的光照效果,同样的把计算结果添加到输出颜色上。我在这里就把CalcSpotLight的实现留作个读者们的练习吧。

设置平行光结构体的uniform值和之前所讲过的方式没什么两样,但是你可能想知道如何设置场景中PointLight结构体的uniforms变量数组。我们之前并未讨论过如何做这件事。

庆幸的是,这并不是什么难题。设置uniform变量数组和设置单个uniform变量值是相似的,只需要用一个合适的下标就能够检索到数组中我们想要的uniform变量了。

glUniform1f(glGetUniformLocation(lightingShader.Program, "pointLights[0].constant"), 1.0f);

这样我们检索到pointLights数组中的第一个PointLight结构体元素,同时也可以获取到该结构体中的各个属性变量。不幸的是这一位置我们还需要手动对这个四个光源的每一个属性都进行设置,这样手动设置这28个uniform变量是相当乏味的工作。你可以尝试去定义个光源类来为你设置这些uniform属性来减少你的工作,但这依旧不能改变去设置每个uniform属性的事实。

别忘了,我们还需要为每个光源设置它们的位置。这里,我们定义一个glm::vec3类的数组来包含这些点光源的坐标:

glm::vec3 pointLightPositions[] = {
    glm::vec3 (0.7f,  0.2f,  2.0f),
    glm::vec3 (2.3f, -3.3f, -4.0f),
    glm::vec3 (-4.0f,  2.0f, -12.0f),
    glm::vec3 (0.0f,  0.0f, -3.0f)
};

同时我们还需要根据这些光源的位置在场景中绘制4个表示光源的立方体,这样的工作我们在之前的教程中已经做过了。

如果你在还使用了手电筒的话,将所有的光源结合起来看上去应该和下图差不多:

你可以在此处获取本教程的源代码,同时可以查看顶点着色器片段着色器的代码。
以及我的项目文件

上面的图片的光源都是使用默认的属性的效果,如果你尝试对光源属性做出各种修改尝试的话,会出现很多有意思的画面。很多艺术家和场景编辑器都提供大量的按钮或方式来修改光照以使用各种环境。使用最简单的光照属性的改变我们就足已创建有趣的视觉效果:

相信你现在已经对OpenGL的光照有很好的理解了。有了这些知识我们便可以创建丰富有趣的环境和氛围了。快试试改变所有的属性的值来创建你的光照环境吧!

练习

  • 创建一个表示手电筒光的结构体Spotlight并实现CalcSpotLight(…)函数:
vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
    vec3 lightDir = normalize(light.position - fragPos);
    // Diffuse shading
    float diff = max(dot(normal, lightDir), 0.0);
    // Specular shading
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    // Attenuation
    float distance = length(light.position - fragPos);
    float attenuation  = 1.0f / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
    // Spotlight intensity
    float theta = dot(lightDir, normalize(-light.direction));
    float epsilon = light.cutOff - light.outerCutOff;
    float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
    // Combine results
    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
    ambient *= attenuation * intensity;
    diffuse *= attenuation * intensity;
    specular *= attenuation * intensity;
    return (ambient + diffuse + specular);
}
  • 你能通过调节不同的光照属性来重新创建一个不同的氛围吗?
    desert场景

注意,在lamp.frag中添加uniform vec3 lampColor,来通过程序指定光源的颜色。

其他场景类似。

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

推荐阅读更多精彩内容

  • 现实世界的光照是极其复杂的,而且会受到诸多因素的影响,这是以目前我们所拥有的处理能力无法模拟的。因此OpenGL的...
    IceMJ阅读 1,942评论 1 6
  • 麻木地活着 生无可恋? 没有痛苦矛盾的生活 或许只存在于梦中 而梦也并非每个都如此美好 如果我自己也不信我能做好 ...
    角落蜷缩阅读 197评论 0 0
  • 今天看到这则新闻 有点心疼 每年的中秋 都是家人团聚的时光 家中的父母更是盼着儿女回来共聚一堂 可是老人在个把月前...
    马田心Martinc手作阅读 211评论 2 1
  • 或许我们曾无数次幻想过自己以后的生活 从我们记事开始 我们就在不停的幻想 小学的时候我们幻想没有留堂作业还有一群好...
    JoJo777阅读 185评论 0 0
  • 到读图035,2017年这几个月拍的照片,就选完了。然后从2016年选。 生活家们喜欢在老街开店,街道慢慢变成特色...
    汉口张叔叔阅读 190评论 0 0