从0开始的OpenGL学习(十三)-光照贴图

本文主要解决一个问题:

如何使用光照贴图给材质添加更多的灵活性?

引言

在上一篇文章中中,我们为整个物体定义了一个整体的材质,但是现实世界中的对象通常不只一种材质,而是有多种材质组成。 想象一辆汽车:车框架是钢制的,还喷了漆,看上去闪亮闪亮的,窗户的部分能照出周围的景物,轮胎是橡胶不那么闪,里面的骨架是钢就亮很多(前提是你洗了车) 。由此可见,物体有很大可能是由不同材质组成的一个整体。难道我们还对物体的每个部分都设置一个材质吗?

当然不是,我们有光照贴图!严格来说,有三种光照贴图:环境光贴图、漫反射光贴图、镜面高光贴图。但是环境光和漫反射光的颜色相似,只是稍微暗淡点,所以我们可以把漫反射光的贴图用到环境光上。剩下的就只有两种贴图了:漫反射光贴图和镜面高光贴图。

漫反射光贴图

还记得我们讲纹理的章节吗?在纹理章节里,我们直接把片元的颜色设置成从纹理种采样的颜色值,而在这章中,我们会对采样后的颜色值再进行一系列的计算,这就是光照贴图(不管是漫反射还是镜面高光)的原理。

由于是对漫反射颜色产生影响,所以我们称之为漫反射光贴图。但是,使用的方法还是类似的。本次我们使用下面的图来进行操作:

使用图片(来自:www.learningopengl.com)

这是一个带金属边的木盒子,至于为什么要金属边,你往下看就知道了。

要使用这张图,我们需要把材质结构中的环境光和漫反射属性去掉,替换成2D纹理图。

struct Material {
    sampler2D diffuse;
    vec3      specular;
    float     shininess;
}; 
...
in vec2 TexCoords;

记住:sampler2D是OpenGL中的隐含类型,我们不能去设置它,只能将它暴露出来让OpenGL自己去设置。如果你强行设置,OpenGL会爆出一大堆乱七八糟的Error,烦都烦死了。

改了材质结构之后,引用的方式自然也得改,从单纯的一个变量引用,现在需要对纹理进行采样了。

vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
vec3 diffuse = light.diffuse * (diff * vec3(texture(material.diffuse, TexCoords)));

环境光也用一样的纹理(如果你非得要用别的纹理,也没什么问题,用同样的方法搞一张就行了。),替换掉material.diffuse和material.ambient之后,就是这样了。

当然,如果你现在就编译运行,是绝对看不到什么效果滴,为啥?因为我们还没有把纹理坐标传递给片元着色器啊!使用纹理当然要为顶点指定纹理坐标,然后将纹理坐标传递给顶点着色器,让顶点着色器将纹理坐标传递给片元着色器。顶点属性已经准备好了,就是这样:

float vertices[] = {
    // 位置                  // 法线                 // 纹理坐标
    -0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  0.0f, 0.0f,
     0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  1.0f, 0.0f,
     0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  1.0f, 1.0f,
     0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  1.0f, 1.0f,
    -0.5f,  0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  0.0f, 1.0f,
    -0.5f, -0.5f, -0.5f,  0.0f,  0.0f, -1.0f,  0.0f, 0.0f,

    -0.5f, -0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   0.0f, 0.0f,
     0.5f, -0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   1.0f, 0.0f,
     0.5f,  0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   1.0f, 1.0f,
     0.5f,  0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   1.0f, 1.0f,
    -0.5f,  0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   0.0f, 1.0f,
    -0.5f, -0.5f,  0.5f,  0.0f,  0.0f, 1.0f,   0.0f, 0.0f,

    -0.5f,  0.5f,  0.5f, -1.0f,  0.0f,  0.0f,  1.0f, 0.0f,
    -0.5f,  0.5f, -0.5f, -1.0f,  0.0f,  0.0f,  1.0f, 1.0f,
    -0.5f, -0.5f, -0.5f, -1.0f,  0.0f,  0.0f,  0.0f, 1.0f,
    -0.5f, -0.5f, -0.5f, -1.0f,  0.0f,  0.0f,  0.0f, 1.0f,
    -0.5f, -0.5f,  0.5f, -1.0f,  0.0f,  0.0f,  0.0f, 0.0f,
    -0.5f,  0.5f,  0.5f, -1.0f,  0.0f,  0.0f,  1.0f, 0.0f,

     0.5f,  0.5f,  0.5f,  1.0f,  0.0f,  0.0f,  1.0f, 0.0f,
     0.5f,  0.5f, -0.5f,  1.0f,  0.0f,  0.0f,  1.0f, 1.0f,
     0.5f, -0.5f, -0.5f,  1.0f,  0.0f,  0.0f,  0.0f, 1.0f,
     0.5f, -0.5f, -0.5f,  1.0f,  0.0f,  0.0f,  0.0f, 1.0f,
     0.5f, -0.5f,  0.5f,  1.0f,  0.0f,  0.0f,  0.0f, 0.0f,
     0.5f,  0.5f,  0.5f,  1.0f,  0.0f,  0.0f,  1.0f, 0.0f,

    -0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,  0.0f, 1.0f,
     0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,  1.0f, 1.0f,
     0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,  1.0f, 0.0f,
     0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,  1.0f, 0.0f,
    -0.5f, -0.5f,  0.5f,  0.0f, -1.0f,  0.0f,  0.0f, 0.0f,
    -0.5f, -0.5f, -0.5f,  0.0f, -1.0f,  0.0f,  0.0f, 1.0f,

    -0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f,  0.0f, 1.0f,
     0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f,  1.0f, 1.0f,
     0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,  1.0f, 0.0f,
     0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,  1.0f, 0.0f,
    -0.5f,  0.5f,  0.5f,  0.0f,  1.0f,  0.0f,  0.0f, 0.0f,
    -0.5f,  0.5f, -0.5f,  0.0f,  1.0f,  0.0f,  0.0f, 1.0f
};

替换之后,在顶点着色器中添加属性的输入:

layout (location = 2) in vec2 aTexCoords;
...
out vec2 TexCoords;

void main()
{
    ...
    TexCoords = aTexCoords;
}  

别忘了添加顶点的纹理属性,然后将一众的属性跨度改成8*sizeof(float)。

// 纹理属性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);

因为我们的项目是开始了光照之后的新鲜货,之前纹理章节中加载图片的代码已经没了,正好我们把这部分的功能封装成一个函数,用起来就简单了。代码已经封装好了,请看:

//加载纹理
unsigned int loadTexture(char const * path){
    unsigned int textureID;
    glGenTextures(1, &textureID);

    int width, height, nrComponents;
    unsigned char * data = stbi_load(path, &width, &height, &nrComponents, 0);
    if (data) {
        GLenum format;
        if (nrComponents == 1)
            format = GL_RED;
        else if (nrComponents == 3)
            format = GL_RGB;
        else if (nrComponents == 4)
            format = GL_RGBA;

        glBindTexture(GL_TEXTURE_2D, textureID);
        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        stbi_image_free(data);
    }
    else {
        std::cout << "纹理加载失败,路径是:" << path << std::endl;
        stbi_image_free(data);
    }

    return textureID;
}

最后,我们需要加载之前的图片做纹理,设置漫反射纹理图,启用这张纹理图:

unsigned int diffuseMap = loadTexture("container2.png");
...
lightingShader.setInt("material.diffuse", 0);
...
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, diffuseMap);

编译运行,不出意外的话,效果应该像这样:

运行效果

如果效果有差,参考这里的完整代码

镜面高光贴图

乍一看效果倒是不错,可是总感觉怪怪,为啥一个木头箱子会有这么亮的反光呢?我们来修复这个问题,将木头的部分反光效果去除,边框金属部分的反光效果保留。这个过程看上去和漫反射贴图一样,巧合?我想不是。

用一张纹理图来充当镜面高光的效果图。我们需要生成一张黑白的纹理图(当然你想用彩色的也没问题),这张图已经准备好了,就是下面这张:

镜面高光纹理图

木头的部分没有镜面高光效果,所以是黑色的,外面的金属框有镜面高光效果,所以其颜色为灰色。

严格来说,木头也是有高光效果的,只是非常微弱,大部分都被散射掉了。我们出于学习的目的,将高光效果设置成了0。

用神器PS或者其他的软件就能做出一张合格的纹理图,所以,是不是考虑一下学个PS:)?

用法和之前几乎没有区别。先来把片元着色器中的代码改一改:

struct Material{
    sampler2D diffuse;
    sampler2D specular;
    float shininess;
};
vec3 specular = light.specular * (spec * vec3(texture(material.specular, TexCoords)));

将材质中的镜面高光改成纹理采样,计算镜面高光的时候也改成纹理采样。

紧接着,在主函数中加载纹理图,设置镜面高光纹理图,将纹理图绑定到纹理单元1上。

unsigned int specularMap = loadTexture("container2_specular.png");
lightingShader.setInt("material.specular", 1);
...
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, specularMap);  

改完之后,编译运行,你会看到类似下面的效果:

运行效果

如果你看到的场景不正确,请查看这里的代码比对。

总结

在本文中,我们学习了如何用光照贴图代替材质的单一反射属性,在同一个物体的不同部分应用不同的材质。使用的方式很简单,对贴图进行采样,然后和光照进行计算。这种方式,和之前纹理章节介绍的内容十分相似,对比之前纹理章节的片元着色器代码,你会有更深的理解。

下一篇
目录
上一篇

参考资料

www.learningopengl.com(非常好的网站,建议学习)

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

推荐阅读更多精彩内容