从0开始的OpenGL学习(十五)-多光源

本文主要解决一个问题:

如何在场景中实现多个光源?

引言

在之前的文章中,我们学了很多OpenGL中的光照知识,包括冯氏着色、材质、光照贴图以及不同类型的光源模型等等。本文中,我们要把这些知识都组合起来,在场景中创造6个光源。我们要创造1个的方向光,4个点光源以及1个聚光灯(手电筒),然后看看整个场景会是什么样子。

封装光源操作

为了使用多个光源,我们将会把光照计算的操作封装进GLSL函数中。如果你是一个新手,可能觉得这不是必要的操作。如果你有一些经验,将代码封装成函数是一件自然而然的事情,这样做不仅结构清晰,而且易于使用。

我们已经学了很多GLSL的语法,但是封装函数还没有学到。不过不用担心,GLSL中的函数和C中的函数很相似,都需要一个函数名,一个返回值,在调用之前需要声明等等。对于三种不同的光源模型,我们定义了3个不同的函数,分别是:CalcDirLight,CalcPointLight和CalcSpotLight。

想想在一个场景中,很多的光源照射到同一个物体上时,物体会呈现出什么样子?多种光的效果会叠加起来,呈现出一种混合的状态,我们试着来总结一个流程:

  1. 一个颜色向量表示片元的输出颜色
  2. 计算每个光源对输出颜色的影响,将所有的结果相加。
  3. 将所有结果的和传递给片元颜色作为最终结果

用伪代码表示这个过程就是这样:

void main(){
  vec3 output = vec3(0,0);
  output += 计算方向光的函数;
  for (int i = 0; i < 点光源数量; ++i)
    output += 计算点光源的函数;
  output += 计算聚光灯的函数;
  FragColor = vec4(output, 1.0);
}

在实现的过程,实际的代码可能与这个不同,不必拘泥于这个代码形式,思路是这样就不会有问题。接下来,我们来定义一些计算不同光源对片元颜色产生影响的函数。

方向光

函数形式非常简单,只需根据输入的参数计算方向光对当前片元颜色的影响并返回结果就行了。不过首先,我们要来定义一个方向光源的结构体。

//方向光源
struct DirLight{
    vec3 direction;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
uniform DirLight dirLight;

之后,将这个方向光源作为参数传递到计算光照的函数中:

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

可以看到,这个函数需要一个DirLight的对象,法线参数,以及观察方向。如果你非常熟悉之前的代码,那么实现这个函数对你来说就轻而易举。

vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir) {
    vec3 lightDir = normalize(-light.direction);
    //环境光
    vec3 ambient = light.ambient * vec3 (texture(material.diffuse, TexCoords));

    //漫反射
    float diff = max(dot(normal, lightDir), 0.0);
    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));

    //镜面高光
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    vec3 specular = light.specular * spec * vec3 (texture(material.specular, TexCoords));

    return (ambient + diffuse + specular);
}

基本上都是复制粘贴之前的代码,然后将代码整理一下的结果。

点光源

和方向光一样,我们先要定义一个点光源的结构,然后创建4个点光源。不同的是,我们采用数组的方式来创建4个点光源。具体实现如下:

//点光源
struct PointLight{
    vec3 position;

    float constant;
    float linear;
    float quadratic;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
#define NR_POINT_LIGHTS 4
uniform PointLight pointLight[NR_POINT_LIGHTS];

如你所见,定义数组的语法也和C类似,笔者觉得会C语言真是太幸运了。当然,我们可以把所有的数据放到一个光源结构中,这样所有的光源都能使用同一个结构。但笔者更倾向于定义不同的结构,这样更简洁,扩展性更好,占用的空间也更少。

计算光照的原型如下:

vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir); 

实现的方式也和之前的代码一样,我们复制粘贴过来,然后做些修改:

//计算点光源的影响
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir){
    //环境光
    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));

    //漫反射光
    vec3 norm = normalize(normal);
    vec3 lightDir = normalize(light.position - FragPos);  
        
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));

    //镜面高光
    vec3 reflectDir = reflect(-lightDir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));

    //衰减
    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;

    return ambient + diffuse + specular;
}

依旧没什么花头,就是前面已经实现过的代码。

聚光灯

这里我们可以偷个懒,因为前一篇文章中,我们最后实现的就是聚光灯,之前又是新增数据结构,没有改之前的Light结构,这里我们只需要将原有的Light结构换个名字成SpotLight就直接获得了一个聚光灯的结构。

然后,定义一个聚光灯的处理函数如下:

//计算聚光灯的影响
vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir){
   //环境光
    vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));

    //漫反射光
    vec3 norm = normalize(normal);
    vec3 lightDir = normalize(light.position - fragPos);  
        
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));

    //镜面高光
    vec3 reflectDir = reflect(-lightDir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));

    //聚光灯
    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 *= intensity;

    //衰减
    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;

    return ambient + diffuse + specular;
}

修改了一些变量名,比如normal,观察方向也不需要计算,直接可以使用了,省了不少事。

整合

根据之前分析的结构,将代码补充完整:

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);

    //聚光灯
    result += CalcSpotLight(spotLight, norm, FragPos, viewDir);
 
    FragColor = vec4(result, 1.0f);
}

在主函数中设置结构体中的元素你已经很熟悉了,那么怎么设置数组中的元素呢?答案很简单,还是C语言的语法,请看下面的代码:

lightingShader.setFloat("pointLights[0].constant", 1.0f);

没错,像C语言访问数组那样访问一个元素,然后设置其值。

别忘了还有点光源的位置我们没设置,快来看看我们把这些点光源放在哪里:

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)
}; 

接下来,我们就要为着色器中的这些元素赋值了。你可能会想,这里面这么多元素,难道要一个一个去赋值吗,有没有更好的方法?不过很遗憾,目前来说我们还没有简单的赋值方法,只能手动赋值,为了避免手写的枯燥,笔者在这里把赋值的代码贴出来:

/*
为光源赋值
*/
// 方向光
lightingShader.setVec3("dirLight.direction", -0.2f, -1.0f, -0.3f);
lightingShader.setVec3("dirLight.ambient", 0.05f, 0.05f, 0.05f);
lightingShader.setVec3("dirLight.diffuse", 0.4f, 0.4f, 0.4f);
lightingShader.setVec3("dirLight.specular", 0.5f, 0.5f, 0.5f);
// 点光源1
lightingShader.setVec3("pointLights[0].position", pointLightPositions[0]);
lightingShader.setVec3("pointLights[0].ambient", 0.05f, 0.05f, 0.05f);
lightingShader.setVec3("pointLights[0].diffuse", 0.8f, 0.8f, 0.8f);
lightingShader.setVec3("pointLights[0].specular", 1.0f, 1.0f, 1.0f);
lightingShader.setFloat("pointLights[0].constant", 1.0f);
lightingShader.setFloat("pointLights[0].linear", 0.09);
lightingShader.setFloat("pointLights[0].quadratic", 0.032);
// 点光源2
lightingShader.setVec3("pointLights[1].position", pointLightPositions[1]);
lightingShader.setVec3("pointLights[1].ambient", 0.05f, 0.05f, 0.05f);
lightingShader.setVec3("pointLights[1].diffuse", 0.8f, 0.8f, 0.8f);
lightingShader.setVec3("pointLights[1].specular", 1.0f, 1.0f, 1.0f);
lightingShader.setFloat("pointLights[1].constant", 1.0f);
lightingShader.setFloat("pointLights[1].linear", 0.09);
lightingShader.setFloat("pointLights[1].quadratic", 0.032);
// 点光源3
lightingShader.setVec3("pointLights[2].position", pointLightPositions[2]);
lightingShader.setVec3("pointLights[2].ambient", 0.05f, 0.05f, 0.05f);
lightingShader.setVec3("pointLights[2].diffuse", 0.8f, 0.8f, 0.8f);
lightingShader.setVec3("pointLights[2].specular", 1.0f, 1.0f, 1.0f);
lightingShader.setFloat("pointLights[2].constant", 1.0f);
lightingShader.setFloat("pointLights[2].linear", 0.09);
lightingShader.setFloat("pointLights[2].quadratic", 0.032);
// 点光源4
lightingShader.setVec3("pointLights[3].position", pointLightPositions[3]);
lightingShader.setVec3("pointLights[3].ambient", 0.05f, 0.05f, 0.05f);
lightingShader.setVec3("pointLights[3].diffuse", 0.8f, 0.8f, 0.8f);
lightingShader.setVec3("pointLights[3].specular", 1.0f, 1.0f, 1.0f);
lightingShader.setFloat("pointLights[3].constant", 1.0f);
lightingShader.setFloat("pointLights[3].linear", 0.09);
lightingShader.setFloat("pointLights[3].quadratic", 0.032);
// 聚光灯
lightingShader.setVec3("spotLight.position", camera.Position);
lightingShader.setVec3("spotLight.direction", camera.Front);
lightingShader.setVec3("spotLight.ambient", 0.0f, 0.0f, 0.0f);
lightingShader.setVec3("spotLight.diffuse", 1.0f, 1.0f, 1.0f);
lightingShader.setVec3("spotLight.specular", 1.0f, 1.0f, 1.0f);
lightingShader.setFloat("spotLight.constant", 1.0f);    lightingShader.setFloat("spotLight.linear", 0.09);
lightingShader.setFloat("spotLight.quadratic", 0.032);
lightingShader.setFloat("spotLight.cutOff", glm::cos(glm::radians(12.5f)));
lightingShader.setFloat("spotLight.outerCutOff", glm::cos(glm::radians(15.0f)));

为光源赋值之后,我们还要把其余的光源也创建出来,代码也很简单,和我们之前创建多个盒子的时候没什么两样。

glBindVertexArray(lightVAO);
for (unsigned int i = 0; i < 4; ++i) {
    glm::mat4 model2;
    model2 = glm::translate(model2, pointLightPositions[i]);
    model2 = glm::scale(model2, glm::vec3(0.2f));
    lampShader.setMat4("model", glm::value_ptr(model2));
    glDrawArrays(GL_TRIANGLES, 0, 36);
}

这里只有一点要注意,就是要使用光源VAO之后再绘制光源立方体。

编译运行,如果没错,你看到的场景应该是这样的:

运行效果

仔细观察,还有手电筒的效果哦!如果效果不一样,请下载源码进行比对。

总结

本文并没有介绍什么新的知识,只是将已有知识进行整合使用,实现一些效果,你也可以在本文的基础上改变光源的颜色值,看看场景会变成什么稀奇古怪的样子,这是一个很有趣的过程。

下一篇
目录
上一篇

参考资料

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

推荐阅读更多精彩内容