Code Learn OpenGL Chapter2

OpenGL第二章

因为第一章写了一万多字Typora就开始有点卡了,所以决定一章一个文件,从现在开始第二章就逐渐进入光照的介绍了。

颜色

OpenGL里对颜色的处理是符合生活常理的,即:眼睛看到的颜色是物体拒绝吸收的光的颜色,也就是说一个蓝色的物体其实是它拒绝吸收蓝色。

当我们把光源的颜色与物体的颜色值相乘,所得到的就是这个物体所反射的颜色(也就是我们所感知到的颜色)。

glm::vec3 lightColor(1.0f, 1.0f, 1.0f);
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (1.0f, 0.5f, 0.31f);

基础光照

现实中的光照非常复杂,出于性能考量,我们在OpenGL中使用的是简化的光照模型,其中一个常用的模型叫做冯氏光照模型(Phong Lighting Model),这种模型由三个分量组成:

环境光照

float ambientStrength = 0.1;
//常量环境因子
vec3 ambient = ambientStrength * lightColor;

漫反射

img

漫反射的原理是,光照越垂直一个物体,它对物体的影响会越大,为了测量这个垂直程度,我们要用到法向量,正如先前所说,两角点乘越接近1,它们的夹角越小,这里用的就是这个原理。

vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
//如果两角为钝角,点乘会返回负数,但是这不是我们想要的结果
vec3 diffuse = diff * lightColor;

在万事大吉之前,还有一件事要注意一下,还记得我们处理模型位置的时候,第一步做了什么吗?从模型空间转换到世界空间。既然模型位置需要转换,法向量要不要转换呢?答案是要。

但是和位置信息不一样,法向量是一个向量,正如定义,向量包含位置信息,所以:

  1. model矩阵的位移操作应该被去除。
  2. 我们应该专注于缩放和旋转变换。

不过如果模型执行了不等比的缩放,会导致法向量不再垂直于表面,为了纠正这个问题,我们使用法线矩阵(Normal Matrix)。

img

法线矩阵实质上是模型矩阵左上角的逆矩阵的转置矩阵,在顶点着色器中我们可以自己生成这个矩阵:

Normal = mat3(transpose(inverse(model))) * aNormal;
//            转置矩阵   逆矩阵   模型矩阵

关于矩阵的证明,这里给出网上找的一幅图,个人觉得解释的不错:

img

镜面光照

所谓镜面光照其实就是高光(Specular Highlight),它的原理如图,结合生活常识应该不难理解:

img

最终高光呈现出来的效果是,夹角越小,镜面光越强烈。

float specularStrength = 0.5;
//镜面强度
vec3 viewDir = normalize(viewPos - FragPos);
//算出入射向量
vec3 reflectDir = reflect(-lightDir, norm);
//算出出射向量,在漫反射的时候我们算的lightDir是从镜面出发到光源位置,所以这里要取反
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
//32指代反光度,反光度越大,散射越少
//这部操作还算了出射向量和观察向量的角度
vec3 specular = specularStrength * spec * lightColor;

算出三个光,我们再将它们汇总,这样就算出了最后的冯氏着色:

vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);

这节的最后再提一下Gouraud着色,Gouraud着色本质是再顶点着色器实现的冯氏着色,相比片段着色器来说,顶点着色器要处理的顶点要少很多,所以Gouraud着色会更加高效,但因为顶点少了,更多的地方要通过线性插值来计算光照颜色,这样处理的光照可以说是靠猜,就不够真实。

材质

对于不同物体,它们会有不同的“反光度”,通过对光的反射,这些属性会让物体呈现出不同的效果,具体来说,这些属性分为:

#version 330 core
struct Material {
    vec3 ambient;
    //环境光反射颜色
    vec3 diffuse;
    //漫反射物体颜色
    vec3 specular;
    //镜面光照颜色影响
    float shininess;
    //高光的半径
}; 
//请注意这里的反射颜色是vec3,方便控制对于每个颜色通道的反射程度

uniform Material material;

改了材质,我们再将光照强度细分为每个通道颜色的强度,请注意区分材质和光照强度,材质是对反射颜色的描述,而光照是对各个颜色的光照强度的描述:

struct Light {
    vec3 position;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

uniform Light light;

更改完之后,本来的代码应该改成:

vec3 ambient  = light.ambient * material.ambient;
//可能有人会疑惑,light.ambient是一个向量,material.ambient也是一个向量,为什么两个向量相乘不是一个数字,而是另外一个向量呢?
//这里就要区分向量的三种乘法了,在OpenGL中'*'表示的是Hadamard乘法,也就是每一个分量之间相乘,具体操作很像向量加法,而向量点乘用的是dot()函数,向量叉乘是cross()函数
vec3 diffuse  = light.diffuse * (diff * material.diffuse);
vec3 specular = light.specular * (spec * material.specular);
//可以看到,光照强度的向量代替了之前的float类型的强度的位置

下面附上完整代码,结合这两张内容,相信不需要注释你也能看的懂:

#version 330 core

in vec3 Normal;
in vec3 FragPos;

out vec4 color;

struct Material {
 vec3 ambient;
 vec3 diffuse;
 vec3 specular;
 float shininess;
};

struct Light {
 vec3 position;
 vec3 ambient;
 vec3 diffuse;
 vec3 specular;
};

uniform vec3 viewPos;
uniform Material material;
uniform Light light;

void main(){
 float ambientStrength = 0.1;
 float specularStrength = 0.8;

 vec3 ambient = light.ambient * material.ambient;

 vec3 lightDir = normalize(light.position - FragPos);
 vec3 norm = normalize(Normal);
 float diff = max(dot(norm, lightDir), 0.0);
 vec3 diffuse = light.diffuse * (diff * material.diffuse);

 vec3 viewDir = normalize(viewPos - FragPos);
 vec3 reflectDir = reflect(-lightDir, norm);
 float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
 vec3 specular = light.specular * spec * material.specular;

 vec3 result = ambient + diffuse + specular;
 color = vec4(result, 1.0);
}

光照贴图

上面的光照贴图确实很好的反射了光照,但是现实生活中一个物体可能会以不同的方式反射光,因为物体的每个部分材质贴图都是不同的,所以我们接下来要引入漫反射镜面光贴图

漫反射贴图从原理上来说就是一个纹理,在着色器中使用漫反射贴图的方法也和纹理贴图一样,此处我们把它也加到Material中:

struct Material {
    sampler2D diffuse;
    //这里补充一点不透明类型的知识,如果要把不透明类型声明到结构体种,那么这个结构体必须要被声明为uniform,详见:https://www.khronos.org/opengl/wiki/Data_Type_(GLSL)#Opaque_types
    vec3      specular;
    float     shininess;
}; 
...
in vec2 TexCoords;

C++代码没有变多少,不过还是展示一下封装过的加载贴图代码:

unsigned int loadTexture(char const * path)
{
   unsigned int textureID;
   glGenTextures(1, &textureID);

   int width, height, nrComponents;
   unsigned char *data = SOIL_load_image(path, &width, &height, &nrComponents, 0);
    //请注意这里最后一个参数是0,表示我们不限定load图片的通道数
   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);
       //因为没有限定load图片的通道数,这里的格式也需要自适应了
       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);

       SOIL_free_image_data(data);
   }
   else
   {
       std::cout << "Texture failed to load at path: " << path << std::endl;
       SOIL_free_image_data(data);
   }

   return textureID;
}

片段着色器只需要小改动:

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

虽然这下立方体有颜色了,但是却很假,因为木头是不应该有高光的,但如果直接把高光的材质设为0,那金属框也不会反射高光了,这个时候我们就需要引入镜面光贴图了。

img

镜面高光的强度取决于每个像素的亮度,这个像素约白,那它反射高光就越强,可以看到木头是不反光的,所以中间都是黑的。

除了镜面发光,我们还可以做放射光贴图,也就是自发光的效果,贴图如下:

img

明白这些贴图各自的作用后,我们回顾一下在循环中怎么传贴图:

while (!glfwWindowShouldClose(window)){
   fflush(stdout);
   currentFrame = glfwGetTime();
   deltaFrame = currentFrame - lastFrame;
   lastFrame = currentFrame;
   glfwPollEvents();
   pos_update();
   glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

   mat4 view = mat4(1.0f);
   view = camera.GetViewMatrix();
   mat4 projection = mat4(1.0f);
   projection = perspective(camera.Zoom, (float)WIDTH/(float)HEIGHT, 0.1f, 100.0f);
   vec3 lightPos(1.2f, 1.0f, 2.0f);
   vec3 resize(0.5f);

   glBindTexture(GL_TEXTURE_2D, texture);
    //这里省略了激活Texture0的代码,因为如果没有还没有激活别的贴图,那么默认当前的激活Texture就是0

   glActiveTexture(GL_TEXTURE1);
    //切换当前激活Texture为1并绑定对应贴图
   glBindTexture(GL_TEXTURE_2D, texture1);

   glActiveTexture(GL_TEXTURE2);
   glBindTexture(GL_TEXTURE_2D, texture2);

   myShader.Use();
   myShader.setMat4("view", view);
   myShader.setMat4("projection", projection);
   glBindVertexArray(VAO);
   mat4 model = mat4(1.0f);
   myShader.setMat4("model", model);

   myShader.setInt("material.diffuse", 0);
   myShader.setInt("material.specular", 1);
   myShader.setInt("material.emission", 2);
    //这里告诉对应sampler它的采样目标是Texture几
   myShader.setFloat("material.shininess", 64.0f);


   myShader.setVec3("light.ambient", vec3(0.2f, 0.2f, 0.2f));
   myShader.setVec3("light.diffuse", vec3(0.5f, 0.5f, 0.5f));
   myShader.setVec3("light.specular", vec3(1.0f, 1.0f, 1.0f));

   vec3 emission = vec3(abs(sin(glfwGetTime())));
   myShader.setVec3("light.emission", emission);
   myShader.setVec3("light.position", lightPos);
   myShader.setVec3("viewPos", camera.Position);

   myShader.setFloat("movement", glfwGetTime() * 0.5);

   glDrawArrays(GL_TRIANGLES, 0, 36);
   glBindVertexArray(0);

   lightShader.Use();

   model = translate(model, lightPos);
   model = scale(model, resize);
   lightShader.setMat4("view", view);
   lightShader.setMat4("projection", projection);
   lightShader.setMat4("model", model);
   glBindVertexArray(lightVAO);
   glDrawArrays(GL_TRIANGLES, 0, 36);
   glBindVertexArray(0);

   glfwSwapBuffers(window);
}

对应的着色器代码是:

#version 330 core

in vec3 Normal;
in vec3 FragPos;
in vec2 myTex;

out vec4 color;

struct Material {
 sampler2D diffuse;
 sampler2D specular;
 sampler2D emission;
 float shininess;
};

struct Light {
 vec3 position;
 vec3 ambient;
 vec3 diffuse;
 vec3 specular;
 vec3 emission;
};

uniform vec3 viewPos;
uniform Material material;
uniform Light light;
uniform sampler2D sam;
uniform float movement;

void main(){
 float ambientStrength = 0.1;
 float specularStrength = 0.8;

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

 vec3 lightDir = normalize(light.position - FragPos);
 vec3 norm = normalize(Normal);
 float diff = max(dot(norm, lightDir), 0.0);
 vec3 diffuse = light.diffuse * (diff * vec3(texture(material.diffuse, myTex)));

 vec3 viewDir = normalize(viewPos - FragPos);
 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, myTex));

 vec3 emission = light.emission * vec3(texture(material.emission, vec2(myTex.x, myTex.y + movement)));

 vec3 result = ambient + diffuse + specular + emission;
 color = vec4(result, 1.0);
}

应该不难看懂,就不解释了。

光源

如标题所示,这节我们会讨论三种光:定向光(Directional Light),点光源(Point Light),聚光(Spotlight)。

定向光

如果光源位置足够远,我们就可以假设它的每条光线是平行的,现实中的例子就是太阳。

img

因为所有光线都平行,所以光源的位置就不重要了,因为对于场景中每一个物体,光的方向都是一致的,所以说代码里我们用光线方向来替代光源位置。

struct Light {
    // vec3 position; // 使用定向光就不再需要了
    vec3 direction;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
...
void main()
{
  vec3 lightDir = normalize(-light.direction);
    //至于这里为什么要取反,请看之前计算漫反射的图
  ...
}

然后再在c++代码中设置direction就行了,对于direction的设计,可以用vec3也可以用vec4,正如之前提到过的四分量向量的第四个分量w,可以在glsl代码中检测,如果w为0就作为定向光处理,如果w为1就作为位置光处理。

点光源

点光源和位置光是特别像的,除了一点,衰减。

之前的位置光无论物体放在哪里,光线的强度都一样,这样是不符合物理常理的,而衰减(Attenuation)是会让光照强度随着距离而减少的一种线性方程: $$ F_{att}=\frac{1.0}{K_c+K_ld+K_qd^2} $$ 在这里d代表了片段距离光源的距离,而三个K则是我们为了计算衰减值的可配置数值:

覆盖距离 常数项 一次项 二次项
7 1.0 0.7 1.8
13 1.0 0.35 0.44
20 1.0 0.22 0.20
32 1.0 0.14 0.07
50 1.0 0.09 0.032
65 1.0 0.07 0.017
100 1.0 0.045 0.0075
160 1.0 0.027 0.0028
200 1.0 0.022 0.0019
325 1.0 0.014 0.0007
600 1.0 0.007 0.0002
3250 1.0 0.0014 0.000007

代码很简单,只需要新加几个参数到结构体就行:

struct Light {
    vec3 position;  

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;

    float constant;
    float linear;
    float quadratic;
};
    
int main(){
    ...
    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;
}

聚光

聚光时位于一个特定位置朝着特定方向照射的光线,可以想象成手电筒和路灯。

在OpenGL中,聚光由一个世界坐标,一个方向和一个切光角(Cutoff Angle)来表示,切光角定义了圆锥的半径,对于每个片段,我们会计算它在不在切光方向之内。

img

#version 330 core

in vec3 Normal;
in vec3 FragPos;
in vec2 myTex;

out vec4 color;

struct Material {
 sampler2D diffuse;
 sampler2D specular;
 float shininess;
};

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

 vec3 ambient;
 vec3 diffuse;
 vec3 specular;

 float constant;
 float linear;
 float quadratic;
};

uniform vec3 viewPos;
uniform Material material;
uniform Light light;

void main(){
 vec3 lightDir = normalize(light.position - FragPos);
 float theta = dot(lightDir, normalize(-light.direction));
 vec3 ambient = light.ambient * vec3(texture(material.diffuse, myTex));
 vec3 result;
 if(theta > light.cutOff){
     //唯一需要注意的一点,为什么这里是>号
     //原因是我们在这里用的是cos,cos里,越接近1的数值,角度越小,如果对此有疑问请查看cos函数的图像
     vec3 norm = normalize(Normal);
     float diff = max(dot(norm, lightDir), 0.0);
     vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, myTex));

     vec3 viewDir = normalize(viewPos - FragPos);
     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, myTex));

     float distance    = length(light.position - FragPos);
     float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));

     diffuse   *= attenuation;
     specular *= attenuation;

     result = ambient + diffuse + specular;
 } else {
     result = ambient;
 }

 color = vec4(result, 1.0);
}

然而这样的效果还不够真实,主要原因光的边缘太硬了,真实的效果应该是越接近聚光方向的光强越大,约边缘光强越弱,这时候就要用到这个函数: $$ I = \frac{\theta - \gamma}{\epsilon} $$ 这里$\epsilon=\phi-\gamma$,内圆锥和外圆锥的余弦值差,最后的结果是当前片段的聚光强度。

$\theta$ $\theta$(角度) $\phi$(内光切) $\phi$(角度) $\gamma$(外光切) $\gamma$(角度) $\epsilon$ $I$
0.87 30 0.91 25 0.82 35 0.91 - 0.82 = 0.09 0.87 - 0.82 / 0.09 = 0.56
0.9 26 0.91 25 0.82 35 0.91 - 0.82 = 0.09 0.9 - 0.82 / 0.09 = 0.89
0.97 14 0.91 25 0.82 35 0.91 - 0.82 = 0.09 0.97 - 0.82 / 0.09 = 1.67
0.83 34 0.91 25 0.82 35 0.91 - 0.82 = 0.09 0.83 - 0.82 / 0.09 = 0.11
0.64 50 0.91 25 0.82 35 0.91 - 0.82 = 0.09 0.64 - 0.82 / 0.09 = -2.0
0.966 15 0.9978 12.5 0.953 17.5 0.966 - 0.953 = 0.0448 0.966 - 0.953 / 0.0448 = 0.29

可以看到,越接近边缘,$I$越接近1,反之越大,最后给出代码,应该不难理解:

    // spotlight (soft edges)
    float theta = dot(lightDir, normalize(-light.direction)); 
    float epsilon = (light.cutOff - light.outerCutOff);
    float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
    //clamp 保证最后结果在0 - 1之间
    diffuse  *= intensity;
    specular *= intensity;
    
    // attenuation
    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;  

多光源

最后,我们把之前学过的几个光源封装成函数,这样就方便多光源的整合计算了。

定向光

结构体

struct DirLight{
    vec3 direction;
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
}

封装函数

vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
    vec3 lightDir = normalize(-light.direction);
    // 漫反射着色
    float diff = max(dot(normal, lightDir), 0.0);
    // 镜面光着色
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    // 合并结果
    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);
}

点光源

结构体

struct PointLight {
    vec3 position;

    float constant;
    float linear;
    float quadratic;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};  

封装函数

vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
    vec3 lightDir = normalize(light.position - fragPos);
    // 漫反射着色
    float diff = max(dot(normal, lightDir), 0.0);
    // 镜面光着色
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    // 衰减
    float distance    = length(light.position - fragPos);
    float attenuation = 1.0 / (light.constant + light.linear * distance + 
                 light.quadratic * (distance * distance));    
    // 合并结果
    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;
    diffuse  *= attenuation;
    specular *= attenuation;
    return (ambient + diffuse + specular);
}

聚光

结构体

struct SpotLight {
    vec3 position;
    vec3 direction;
    float cutOff;
    float outerCutOff;
  
    float constant;
    float linear;
    float quadratic;
  
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;       
};

封装函数

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.0 / (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);
}

投影纹理

本章最后补充一个知识点,投影纹理。

投影纹理的效果就和现实中的投影仪一样,可以将图片映射到物体上。

首先,我们以投影源位置为中心定义一个坐标系统,因为经过投影变换的点的范围是[-1,1],所以要通过以下的变换将其转换成范围为[0,1]的UV坐标,变换矩阵如下: $$ M=\left[ \begin{array}{cccc} 0.5 & 0 & 0 & 0.5 \\\ 0 & 0.5 & 0 & 0.5 \\\ 0 & 0 & 0.5 & 0.5 \\\ 0 & 0 & 0 & 1 \end{array} \right]PV $$ 其中P为透视矩阵,V为投影源的LookAt矩阵。

想象一下不难解释为什么需要用到透视矩阵(实际上正交也可以,但是效果不真实),投影的几何体和摄像机的几何体都是锥体。

img

最后附上关键代码:

mat4 projScaleTran = mat4(1.0f);
projScaleTran = translate(projScaleTran, vec3(0.5f));
projScaleTran = scale(projScaleTran, vec3(0.5f));
...
while(...){
    ...
    mat4 m = projScaleTran * perspective(30.0f, 1.0f, 0.2f, 1000.0f) * camera.GetViewMatrix();
    myShader.setMat4("projectorMatrix", m);
    ...
}
//vertex
 ProjTexCoord = projectorMatrix * (model * vec4(position, 1.0f));
 //要随着物体,物体空间变换到世界空间,所以这一步是必要的
//fragment
 vec4 projTexColor = vec4(0.0);
 projTexColor = textureProj(material.projectorTex, ProjTexCoord);

Assimp

在导入模型之前,我们先要了解一个模型导入库:AssimpAssimp可以导入多种不同的模型格式,并将模型数据加载到它的通用数据结构中,加载完之后,我们就可以从这个数据结构中读取我们需要的数据了。

一个模型通常包括多个网格,通常每个模型都由几个子模型、形状组合而成,每个单独的形状就是一个网格。网格是OpenGL中绘制物体所需的最小单位(顶点数据,索引,材质属性)。

img

网格

正如上一节所说,网格代表的是单个可绘制的实体,回想一下网格需要的数据:一系列顶点,每个顶点拥有对应的的位置向量,法向量和纹理坐标向量。用于索引绘制的索引和纹理形式的材质数据。

//
// Created by herain on 7/12/21.
//

#ifndef OPENGL_MESH_H
#define OPENGL_MESH_H

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <string>
#include <vector>
#include "Shader.h"

using namespace glm;
using namespace std;

struct Vertex{
   vec3 Position;
   vec3 Normal;
   vec2 TexCoords;
};

struct Texture{
   unsigned int id;
   //这个id指的是以前我们用SOIL读取图片之后的返回值,即索引texture的值
   string type;
   //这个type会指明这个材质是漫反射还是镜面反射,之后这个会被用于命名
   string path;
};

class Mesh{
public:
   //网格数据
   vector<Vertex> vertices;
   vector<unsigned int> indices;
   vector<Texture> textures;
   //函数
   Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures);
   void Draw(Shader shader);

private:
   //渲染数据
   unsigned int VAO, VBO, EBO;
   //函数
   void setupMesh();
};

Mesh::Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures){
   this->vertices = vertices;
   this->indices = indices;
   this->textures = textures;
   setupMesh();
}

void Mesh::setupMesh() {
   glGenVertexArrays(1, &VAO);
   glGenBuffers(1, &VBO);
   glGenBuffers(1, &EBO);

   glBindVertexArray(VAO);
   glBindBuffer(GL_ARRAY_BUFFER, VBO);
   glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);

   glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
   glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);

   glEnableVertexAttribArray(0);
   glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*) 0);

   glEnableVertexAttribArray(1);
   glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*) offsetof(Vertex, Normal));

   glEnableVertexAttribArray(2);
   glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*) offsetof(Vertex, TexCoords));

   glBindVertexArray(0);
}

void Mesh::Draw(Shader shader) {
   unsigned int diffuseNr = 1;
   unsigned int specularNr = 1;
   for(unsigned int i = 0; i < textures.size(); i++){
       glActiveTexture(GL_TEXTURE0 + i);
       string number;
       string name = textures[i].type;
       if(name == "texture_diffuse")
           number = to_string(diffuseNr++);
       else if(name == "texture_specular")
           number = to_string(specularNr++);

       shader.setFloat(("material."+name+number).c_str(), i);
       //每个漫反射纹理会被命名为texture_diffuseN,每个镜面反射纹理会被命名为texture_specularN
       glBindTexture(GL_TEXTURE_2D, textures[i].id);
       //将纹理数据传入到缓冲中,此时缓冲绑定着某个纹理
   }
   glActiveTexture(GL_TEXTURE0);

   glBindVertexArray(VAO);
   glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
   glBindVertexArray(0);
}

#endif //OPENGL_MESH_H

模型

如之前所说,一个模型是由多个网格构成的,所以在模型类中,就一定会包含一个网格的vector。在这一节中,我们会学习如何将3D模型转换成Mesh对象。

#ifndef MODEL_H
#define MODEL_H

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <stb/stb_image.h>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <map>
#include <vector>
#include "Mesh.h"

using namespace std;

unsigned int TextureFromFile(const char *path, const string &directory);

class Model
{
public:
   vector<Texture> textures_loaded;
    //存储已经加载过的贴图,因为很多情况下同一张贴图会被用很多次,所以不重复加载贴图能在一定程度上提升性能
   vector<Mesh>    meshes;
   string directory;

   Model(string const &path, bool gamma = false)
   {
       loadModel(path);
   }

   void Draw(Shader &shader)
   {
       for(unsigned int i = 0; i < meshes.size(); i++)
           meshes[i].Draw(shader);
   }
    //渲染模型实际上就是把它下面的网格一个个都绘制好

private:
   void loadModel(string const &path)
   {
       Assimp::Importer importer;
       const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate|aiProcess_GenSmoothNormals|aiProcess_FlipUVs);
       //声明了一个加载器,用于加载模型,正如之前提到过,大部分信息都会存在scene里面
       //aiProcess_Triangulate表示如果模型不是完全由三角形组成,就先把它们变成三角形
       //aiProcess_GenSmoothNormals给所有顶点生成平滑的法线
       //aiProcess_FlipUVs会反转贴图的y轴,因为OpenGL里面很多图像都是反的
       //aiProcess_GenNormals如果模型不包含法线就为每个顶点创建法线
       //aiProcess_SplitLargeMeshes将大的网格分成小网格
       //aiProcess_OptimizeMeshes将小网格合并为大的网格,减少绘制调用次数
       //更多的flags可以在这里找到:http://assimp.sourceforge.net/lib_html/postprocess_8h.html
       if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
       {
           cout << "ERROR::ASSIMP:: " << importer.GetErrorString() << endl;
           return;
       }
       //在这一步我们会检查场景和根节点有没有正确读取,以及用一个标记来检查数据完不完整

       directory = path.substr(0, path.find_last_of('/'));
       processNode(scene->mRootNode, scene);
       //将根节点传入递归的函数一个个处理
   }

   void processNode(aiNode *node, const aiScene *scene)
   {
       for(unsigned int i = 0; i < node->mNumMeshes; i++)
       {
           aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
           meshes.push_back(processMesh(mesh, scene));
           //处理当前节点下的每一个网格,并把处理完的网格存储起来
       }

       for(unsigned int i = 0; i < node->mNumChildren; i++)
       {
           processNode(node->mChildren[i], scene);
           //递归处理下面的节点
       }

   }

    //下面是将aimesh对象转换成我们自己的网格对象,我们需要访问网格的相关属性并把它们对应放进我们的对象中
   Mesh processMesh(aiMesh *mesh, const aiScene *scene)
   {
       vector<Vertex> vertices;
       vector<unsigned int> indices;
       vector<Texture> textures;

       //对于网格下每一个节点,我们逐个处理
       for(unsigned int i = 0; i < mesh->mNumVertices; i++)
       {
           Vertex vertex;
           glm::vec3 vector;
           
           //将节点位置读取出来
           vector.x = mesh->mVertices[i].x;
           vector.y = mesh->mVertices[i].y;
           vector.z = mesh->mVertices[i].z;
           vertex.Position = vector;
           
           //如果有法线,就把法线数据读取出来
           if (mesh->HasNormals())
           {
               vector.x = mesh->mNormals[i].x;
               vector.y = mesh->mNormals[i].y;
               vector.z = mesh->mNormals[i].z;
               vertex.Normal = vector;
           }
           
           //如果有纹理信息,就把纹理位置信息读取出来,在这里我们假设只会用到一组纹理位置,请不要搞混这里的纹理位置和纹理,纹理位置是描述了纹理应该怎么贴,纹理是描述了贴什么
           if(mesh->mTextureCoords[0])
           {
               glm::vec2 vec;
               vec.x = mesh->mTextureCoords[0][i].x;
               vec.y = mesh->mTextureCoords[0][i].y;
               vertex.TexCoords = vec;
           }
           else
               vertex.TexCoords = glm::vec2(0.0f, 0.0f);
           vertices.push_back(vertex);
       }
      
       //在Assimp中,每一个面代表了一个图元,我们之前定义过,所以在这个示例中,图元永远是三角形
       //一个面包括了多个索引,索引描述了我们绘制面的顺序,我们只需要把这些顺序存起来就行了
       for(unsigned int i = 0; i < mesh->mNumFaces; i++)
       {
           aiFace face = mesh->mFaces[i];
           for(unsigned int j = 0; j < face.mNumIndices; j++)
               indices.push_back(face.mIndices[j]);
       }
       
       //下面处理材质
       aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
       //下面我们假设采样器的名字为texture_材质类型N
       // diffuse: texture_diffuseN
       // specular: texture_specularN
       // normal: texture_normalN

       //纹理的索引被存在mMaterials里,下面我们从mMaterials中读取对应的贴图
       // 1. diffuse maps
       vector<Texture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");
       textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
       // 2. specular maps
       vector<Texture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");
       textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());

       return Mesh(vertices, indices, textures);
       //返回网格信息
   }

   
   vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName)
   {
       vector<Texture> textures;
       for(unsigned int i = 0; i < mat->GetTextureCount(type); i++)
       {
           //这里GetTextureCount会检测储存在材质中纹理的数量
           aiString str;
           mat->GetTexture(type, i, &str);
           //获取具体文件的位置
           bool skip = false;
           
           //下面会在textures_loaded里面找有没有同样路径的贴图,如果有就代表已经加载过了
           for(unsigned int j = 0; j < textures_loaded.size(); j++)
           {
               if(std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0)
               {
                   textures.push_back(textures_loaded[j]);
                   skip = true;
                   break;
               }
           }
           if(!skip)
           {
               //如果还没加载过则加载
               Texture texture;
               texture.id = TextureFromFile(str.C_Str(), this->directory);
               texture.type = typeName;
               texture.path = str.C_Str();
               textures.push_back(texture);
               textures_loaded.push_back(texture);
           }
       }
       return textures;
   }
};


unsigned int TextureFromFile(const char *path, const string &directory)
{
   string filename = string(path);
   filename = directory + '/' + filename;

   unsigned int textureID;
   glGenTextures(1, &textureID);

   int width, height, nrComponents;
   unsigned char *data = stbi_load(filename.c_str(), &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 << "Texture failed to load at path: " << path << std::endl;
       stbi_image_free(data);
   }

   return textureID;
}
#endif

后记

到此为止第二部分翻译和改编也完成了,下面的内容会更深入地讨论一些关于着色器的问题