京山游侠

专注技术 拒绝扯淡
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Linux 下的 OpenGL 之路(八):贴图和材质

Posted on 2023-07-27 10:47  京山游侠  阅读(228)  评论(0编辑  收藏  举报

前言

按道理,材质和贴图是影响光照的。上一节,我只是简单地弄了一下光照,主要是因为偷懒,主要是想早一点看到 3D 模型的效果。如果要考虑到贴图和材质,需要额外做一点工作。我这里还是使用 Assimp 库载入了我之前收集的一个小木屋模型(是.obj格式的),然后使用 stb_image.h 这个库载入图片进行贴图。多的话不说,先展示效果:

这一节我就不贴代码了,主要是讲一些贴图、材质、光照和我自己的一些设计思路。

简单的光照原理

在现在的 OpenGL 版本中,光照都是写到 Shader 中的,固定管线已经过时。当然,我们需要把计算光照所需要的一些信息传递到 Shader Program 中。究竟有哪些数据是需要传递到 Shader Program 中的呢?我们这里按照简单的光照模型总结一下:

  1. 顶点的位置,这个不用多说,这是组成模型的基础;
  2. 顶点的法线向量。法线向量在光照的计算中非常重要。简单的光照可以理解为包含三个部分:环境光、漫反射光、镜面反射光。其中,漫反射光的强度和光源的方向和法线方向有关,我们一般会将光源方向和法线方向点乘,然后再和模型的漫反射颜色、灯光的颜色相乘。镜面反射光和光源方向、视线方向、法线方向都有关,一般是先计算视线方向和光源方向的中间向量,然后把这个中间向量和法线方向点乘,然后再做一个幂运算,然后和模型的镜面反射颜色、灯光颜色相乘。环境光和法线方向无关,等于模型的环境光颜色和场景的环境光颜色点乘。最后,将以上三个部分相加即可。(这里需要注意,所有的方向向量点乘之前,需要将其长度规范化为1。其次,如果是点光源,还要考虑距离衰减,我们这里暂时忽略。)根据以上原理,其伪代码基本如下:
in vec3 fNormal;
uniform vec3 ambientColor;
uniform vec3 ka;
uniform vec3 kd;
uniform vec3 ks;
uniform vec3 lightDirection;
uniform vec3 eyeDirection;
uniform float shiness;
ambient = ka * ambientColor;
diffuse = kd * dot(normalize(fNormal), normalize(lightDirection)) * lightColor;
speculuar = ks * pow(dot(normalize(fNormal), normalize((lightDirection + eyeDirection)/2)), shiness) * lightColor;
fColor = ambient + diffuse + specular;

当然,其中每一步都要判断是否大于0.0小于1.0。

  1. 根据以上简单光照的原理,我们的模型需要将模型固有的环境光颜色、漫反射颜色、镜面反射颜色、反射指数等数据传入 Shader Program。根据惯例,我们可以把它们简称ka、kd、ks。
  2. 根据以上简单光照的原理,我们的场景需要将灯光位置、灯光颜色、环境光颜色传入 Shader Program。
  3. 除了直接传入模型的颜色值,还可以使用纹理贴图,所以顶点需要纹理坐标,需要将纹理坐标传入 Shader Program。
  4. 纹理贴图需要绑定到纹理单元,然后在 Shader 中使用。根据惯例,我们可以把它们简称为 mapKa、mapKd、mapKs。如果使用纹理,其 Shader 的伪代码基本如下:
in vec3 fNormal;
in vec2 fTexCoords;
uniform vec3 ambientColor;
uniform vec3 ka;
uniform vec3 kd;
uniform vec3 ks;
uniform vec3 lightDirection;
uniform vec3 eyeDirection;
uniform float shiness;

uniform int hasMapKs;
uniform sampler2D mapKs;
uniform int hasMapKd;
uniform sampler2D mapKd;

ambient = ka * ambientColor;
if(hasMapKd){
    diffuse = texture(mapKd, fTexCoords) * dot(normalize(fNormal), normalize(lightDirection)) * lightColor;
    ambient = texture(mapKd, fTexCoords) * ambientColor; //没有单独的 mapKa,所以有 mapKd 的时候直接使用 mapKd 计算环境光。
}else{
    diffuse = kd * dot(normalize(fNormal), normalize(lightDirection)) * lightColor;
}
if(hasMapKs){
    speculuar = texture(mapKs, fTexCoords) * pow(dot(normalize(fNormal), normalize((lightDirection + eyeDirection)/2)), shiness) * lightColor;
}else{
    speculuar = ks * pow(dot(normalize(fNormal), normalize((lightDirection + eyeDirection)/2)), shiness) * lightColor;
}
fColor = ambient + diffuse + specular;

因为不是所有的模型都有贴图,所以在 Shader 里面判断一下,如果有贴图,就使用 mapKd 和 mapKs,否则,就直接使用 kd 和 ks 的值。

在这里,这些 ka、kd、ks、mapKd、mapKs、shiness 等,就构成了模型的材质信息。

我的设计思路

现实中的模型都是千奇百怪的,而我的程序不可能都满足,所以必须做一点假设,如下:

  1. 假设所有模型都具有 ka、kd、ks 和 ns,如果没有,则假设 ka、kd、ks 都是 vec3(1.0f),ns 是 float(100.0f);
  2. 假设所有模型都只使用 mapKd 和 mapKs,不使用其它类型的贴图;没有 mapKa,所以计算环境光颜色的时候,直接使用 mapKd。
  3. 假设对于每一个 Mesh,mapKd 和 mapKs 每种最多只能有一个,不同时使用多个同种类型的贴图;
  4. 假设所有的顶点格式都是提供位置、法线、纹理坐标信息。没有提供法线信息的,Assimp 库在载入模型的时候,可以自动生成。

我使用的是 Assimp 库载入 3D 模型,我用到的 3D 模型都是在网上下载的,有的有贴图和材质信息,有的没有,甚至有的提供有纹理贴图文件,但是 .mtl 文件中却没有把文件路径写上去。这都不是问题,.obj 文件和 .mtl 文件都是纯文本格式的,我们可以自己改。

我使用 stb_image.h 载入纹理贴图文件。对于每个文件,我将它载入内存后,就直接绑定到相应的纹理单元,所以,最后传递给 Mesh 类保存的,只需要 textureID 即可。为了防止重复载入文件,提升效率,我还是保存了纹理贴图文件的路径。

我使用 glm 库来做矩阵和向量的运算。

最后,我觉得每一个 Model 关联一个 Shader Program。一个 Model 里面包含很多个 Mesh ,但是同一个 Model 里面的 Mesh 都使用同一个 Shader Program 进行渲染,当然,不同的 Mesh 可以给 Shader Program 传递不同的参数。

所以,先定义几个简单的结构,其伪代码如下:

struct Vetex{
    glm::vec3 position;
    glm::vec3 normal;
    glm::vec2 texCoord;
}

struct Texture{
    GLunit id;
    std::string path;
}

Model 类的伪代码大概如下:

class Model{
    private:
        std::vector<Mesh> meshes;  //该 Model 中包含的所有 Mesh
        std::vector<Texture> texture_loaded;  //所有已加载的 Texture,为了加速
        ShaderProgram shaderProgram;

    public:
        void loadModel(std::string path);
        void render();
}

而重点是 Mesh 类,其伪代码大概如下:

class Mesh{
    private:
        std::vector<Vetex> vetecis;
        std::vector<glm::vec2> indices;
        GLuint VAO,VBO,EBO,textureId;
        glm::vec3 ka;
        glm::vec3 kd;
        glm::vec3 ks;
        float ns;
        int hasMapKd;
        GLunit mapKd;
        int hasMapKs;
        GLunit mapKs;
        ShaderProgram shaderProgram;

    public:
        void genericMesh(int slice);
        void setup();
        void render();
}

这里的 setup() 方法很重要,需要它将顶点数据存入到 VAO、VBO 和 EBO,然后才能调用 render()。而纹理倒是不用担心,在载入模型的时候,就已经将纹理绑定到指定的纹理单元了,只需要把 textureId 传递到 Shader Program 中即可,就可以找到对应的采样器。

总结

其实 OpenGL 不难,主要是我太懒。但是到这一步,我们就基本上搭建了一个比较完善的框架,可以载入 3D 模型,有适当的纹理和光照,可以看到实实在在的 3D 效果了。这真是一个激动人心的时刻。

下一步,我们来挑战天空盒、反射和折射。

版权申明

该随笔由京山游侠在2023年07月27日发布于博客园,引用请注明出处,转载或出版请联系博主。QQ邮箱:1841079@qq.com