前置:OpenGL基础20:镜面光照
不一定所有的光源都是简单的白光,不仅如此,光线也是可以多次反射的,例如一面镜子,可以从中看到远处的风景,一些金属材质的物体表面也会反射周围物体的光
这主要就是着色器的改动,和漫反射以及镜面反射一样,还有一种贴图叫做反射贴图,当然了,是否是贴图只是着色器写法上的问题,有了之前的经验,搞定这个不是问题
反射的计算方式和之前的镜面反射很像,,着色器如下:
#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
out vec3 normalIn;
out vec3 fragPosIn;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
fragPosIn = vec3(model * vec4(position, 1.0f));
normalIn = mat3(transpose(inverse(model))) * normal;
}
///////////////////////////////////////////////////////////////////////////
#version 330 core
in vec3 normalIn;
in vec3 fragPosIn;
out vec4 color;
uniform vec3 cameraPos;
uniform samplerCube skybox;
void main()
{
float ratio = 1.00 / 1.52;
vec3 I = normalize(fragPosIn - cameraPos);
vec3 R = reflect(I, normalize(normalIn));
color = texture(skybox, R);
}
可以在场景中放一块六边形镜子,它会反射远处的天空盒:
如果一些模型表面不是全反射,必然需要反射贴图(reflection map),并从中获取反射率
折射和反射一样简单,插入水中的筷子会变形,这就是折射
折射遵守斯涅尔定律,看起来就像这样:
观察向量的方向在进入物体表面后有轻微弯曲,弯曲的角度取决于折射指数。每个材质都有自己的折射指数,最常见的:水的折射指数为1.33,玻璃则为1.52、而空气的折射指数为1.00
只需要略微修改片段着色器就可以实现折射:
#version 330 core
in vec3 normalIn;
in vec3 fragPosIn;
out vec4 color;
uniform vec3 cameraPos;
uniform samplerCube skybox;
void main()
{
float ratio = 1.00 / 1.52;
vec3 I = normalize(fragPosIn - cameraPos);
vec3 R = refract(I, normalize(normalIn), ratio);
color = texture(skybox, R);
}
前置:OpenGL基础28:模型
如果尝试使用之前的Mesh类和Model类读取模型,又或者是复制的openGL.cn以及网上的大部分mesh类以及model类,那么你可能会发现一个问题:从网上下载下来的大部分模型资源,它的文件内其实是有材质贴图的,但是读取的时候并读不到任何材质贴图,又或者干脆连贴图都没有
原因有三:
对于情况①,改下Model.h,支持自己传入路径就ok
对于情况③,想想Unity3D,是不是在大部分情况下材质都是我们自己指定的?是的,一般来讲导入模型可不是简单的流水线式硬编码(这里只是为了学习),往往都有一个类编辑器之类的东西,让使用者可以自由设置材质,所以有些模型不会绑定贴图资源。当然了这里还有第二种情况,那就是模型有绑定,但是读取需要到另一个编辑文件中,例如.mtl文件,然而网上大部分的Mesh类和Model类都没有考虑这一点
想要解决这个问题,目前写个编辑器(游戏引擎)好像不太现实,可以稍微处理一下Model.h和Mesh.h以支持自己读取贴图材质,代码如下:
#ifndef MODEL_H
#define MODEL_H
#include
#include
#include"Shader.h"
#include"Mesh.h"
#include
#include
#include
#include
#include
#include
#include
using namespace std;
GLint TextureFromFile(const char* path, string directory, string typeName);
class Model
{
public:
Model(const GLchar* path, const GLchar* texPath = "")
{
this->loadModel(path, texPath);
}
void Draw(Shader shader)
{
for (GLuint i = 0; i < this->meshes.size(); i++)
this->meshes[i].Draw(shader);
}
private:
ifstream myfile;
vector meshes;
string directory;
vector textures_loaded;
void loadModel(string path, string texPath)
{
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
if (!scene || scene->mFlags == AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
{
cout << "ERROR::ASSIMP:: " << importer.GetErrorString() << endl;
return;
}
this->directory = path.substr(0, path.find_last_of('/'));
if (texPath != "")
this->directory = texPath;
myfile.open(texPath + "/index.txt");
this->processNode(scene->mRootNode, scene);
myfile.close();
}
//依次处理所有的场景节点
void processNode(aiNode* node, const aiScene* scene)
{
for (GLuint i = 0; i < node->mNumMeshes; i++)
{
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
this->meshes.push_back(this->processMesh(mesh, scene));
}
for (GLuint i = 0; i < node->mNumChildren; i++)
this->processNode(node->mChildren[i], scene);
}
//将所有原始的aimesh对象全部转换成我们自己定义的网格对象
Mesh processMesh(aiMesh* mesh, const aiScene* scene)
{
vector vertices;
vector indices;
vector textures;
//处理顶点坐标、法线和纹理坐标
for (GLuint 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;
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;
//暂时只考虑第一组纹理坐标,Assimp允许一个模型的每个顶点有8个不同的纹理坐标,只是可能用不到
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);
}
//处理顶点索引
for (GLuint i = 0; i < mesh->mNumFaces; i++)
{
aiFace face = mesh->mFaces[i];
for (GLuint j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}
//处理材质
if (mesh->mMaterialIndex >= 0)
{
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
vector diffuseMaps = this->loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");
textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
vector specularMaps = this->loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
vector reflectionMaps = this->loadMaterialTextures(material, aiTextureType_AMBIENT, "texture_reflection");
textures.insert(textures.end(), reflectionMaps.begin(), reflectionMaps.end());
if (textures.size() == 0 && myfile.is_open())
{
string name, type;
myfile >> type;
myfile >> name;
if (type.length() > 1)
{
aiString str("Img/" + name);
textures.push_back(loadTexturesFromPath(type, str));
}
}
}
return Mesh(vertices, indices, textures);
}
//遍历所有给定纹理类型的纹理位置,获取纹理的文件位置,然后加载生成纹理
vector loadMaterialTextures(aiMaterial* mat, int type, string typeName)
{
vector textures;
for (GLuint i = 0; i < mat->GetTextureCount((aiTextureType)type); i++)
{
aiString str;
mat->GetTexture((aiTextureType)type, i, &str);
textures.push_back(loadTexturesFromPath(typeName, str));
}
return textures;
}
Texture loadTexturesFromPath(string typeName, aiString path)
{
Texture texture;
for (GLuint j = 0; j < textures_loaded.size(); j++)
{
if (std::strcmp(textures_loaded[j].path.C_Str(), path.C_Str()) == 0)
return textures_loaded[j];
}
texture.id = TextureFromFile(path.C_Str(), this->directory, typeName);
texture.type = typeName;
texture.path = path;
this->textures_loaded.push_back(texture);
return texture;
}
};
GLint TextureFromFile(const char* path, string directory, string typeName)
{
string filename = string(path);
filename = directory + '/' + filename;
cout << typeName << ":" << filename << endl;
GLuint textureID;
glGenTextures(1, &textureID);
int width, height;
unsigned char* image = SOIL_load_image(filename.c_str(), &width, &height, 0, SOIL_LOAD_RGB);
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
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);
glBindTexture(GL_TEXTURE_2D, 0);
SOIL_free_image_data(image);
return textureID;
}
#endif
#ifndef MESH_H
#define MESH_H
#include
#include
#include
#include
#include"Shader.h"
#include
#include
#include
#include
#include
#include
using namespace std;
struct Vertex
{
glm::vec3 Position; //顶点
glm::vec3 Normal; //法线
glm::vec2 TexCoords; //贴图
};
struct Material
{
glm::vec4 Ka; //材质颜色
glm::vec4 Kd; //漫反射
glm::vec4 Ks; //镜面反射
};
struct Texture
{
GLuint id;
string type; //贴图类型:漫反射贴图还是镜面贴图(后面还有法线贴图、错位贴图等)
aiString path; //贴图路径
};
class Mesh
{
public:
vector vertices;
vector indices; //索引
vector textures;
Mesh(vector vertices, vector indices, vector textures)
{
this->vertices = vertices;
this->indices = indices;
this->textures = textures;
this->setupMesh();
}
void Draw(Shader shader)
{
GLuint diffuseNr = 1;
GLuint specularNr = 1;
GLuint reflectionNr = 1;
for (GLuint i = 0; i < this->textures.size(); i++)
{
glActiveTexture(GL_TEXTURE0 + i);
stringstream ss;
string name = this->textures[i].type;
if (name == "texture_diffuse")
ss << diffuseNr++;
else if (name == "texture_specular")
ss << specularNr++;
else if (name == "texture_reflection")
ss << reflectionNr++;
name = name + ss.str();
glUniform1i(glGetUniformLocation(shader.Program, name.c_str()), i);
//这样的话,着色器中的纹理名就必须有一个对应的规范,例如“texture_diffuse3”代表第三个漫反射贴图
//方法不唯一,这是最好理解/最简单的一种规范/写法
glBindTexture(GL_TEXTURE_2D, this->textures[i].id);
}
glUniform1f(glGetUniformLocation(shader.Program, "material.shininess"), 16.0f); //暂时写死反光度,也可配置
glBindVertexArray(this->VAO);
glDrawElements(GL_TRIANGLES, this->indices.size(), GL_UNSIGNED_INT, 0); //EBO绘制
for (GLuint i = 0; i < this->textures.size(); i++)
{
glActiveTexture(GL_TEXTURE0 + i);
glBindTexture(GL_TEXTURE_2D, 0);
}
glBindVertexArray(0);
}
private:
GLuint VAO, VBO, EBO;
void setupMesh()
{
glGenVertexArrays(1, &this->VAO);
glGenBuffers(1, &this->VBO);
glGenBuffers(1, &this->EBO);
glBindVertexArray(this->VAO);
glBindBuffer(GL_ARRAY_BUFFER, this->VBO);
glBufferData(GL_ARRAY_BUFFER, this->vertices.size() * sizeof(Vertex), &this->vertices[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, this->indices.size() * sizeof(GLuint), &this->indices[0], GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
//别忘了struct中内存是连续的
//offsetof():获取结构体属性的偏移量
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)offsetof(Vertex, Normal));
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)offsetof(Vertex, TexCoords));
glBindVertexArray(0);
}
};
#endif
当然方法有点拙略,但能解决问题,如果实际模型有贴图,但是读不到,需要自己建立一个Index.txt文件,并在其中输入你想要加载的纹理类型和图片路径,一个例子如下:
总共10个mesh节点,其中第3个节点不需要贴图,其它9个节点贴图都指定为漫反射贴图Img_1.jpg
好了,搞定了,有了这个之后你甚至可以随意指定物体任意一个mesh的贴图,实现“拿漫反射贴图当成折射贴图”之类的骚操作,也可以在这基础上优化这份代码
如果物体存在折射/反射贴图,一个片段着色器的例子如下:(来源于openGL.cn)
#version 330 core
in vec3 Normal;
in vec3 Position;
in vec2 TexCoords;
uniform vec3 cameraPos;
uniform sampler2D texture_diffuse1;
uniform sampler2D texture_reflection1;
uniform samplerCube skybox;
out vec4 color;
void main()
{
// Diffuse
vec4 diffuse_color = texture(texture_diffuse1, TexCoords);
// Reflection
vec3 I = normalize(Position - cameraPos);
vec3 R = reflect(I, normalize(Normal));
float reflect_intensity = texture(texture_reflection1, TexCoords).r;
vec4 reflect_color;
if(reflect_intensity > 0.1) // Only sample reflections when above a certain treshold
reflect_color = texture(skybox, R) * reflect_intensity;
// Combine them
color = diffuse_color + reflect_color;
}
反射和折射一直都是一个很难的问题,本身属于高级光照的范畴,上面只算最基础的了解了
动态环境映射(Dynamic Environment Mapping):
上面的着色器只能反射天空盒,而不能反射周边的物体,这明显是有问题的,不仅如此,周边的物体有些也可能是在运动的,因此想要想要每时每刻正确反射周边的所有物体(场景)并非是件容易的事
一个解决方案就是动态环境映射:使用帧缓冲为物体的所有6个不同角度创建一个场景的纹理,并在每次渲染时迭代储存为一个立方体贴图,之后再使用这个动态生成的立方体贴图来创建真实的反射和折射表面,这样就能包含所有其他物体了,当然了,这方法一听就非常的耗,因此优化是必不可少的
放大镜、凸透镜、半透反射:
字面意思,实现这些物体往往都有特定的算法,不唯一,表现效果和性能都有差
光线追踪(ray tracing):
目前非常有名的一项技术,解决了物体之间多次折射反射的问题,也考虑到了包括衰减在内的各种复杂物理因素,已经不再属于光栅化范畴
不过,现在的游戏基本都没有应用光线追踪技术,光线都是由你能看到的亮光的物体自身发出的,往往不会去深入考虑每个光源从哪里来,到哪里去,更不会计算这些光源的相互叠加,只是通过及时演算物体阴影和控制光线的强弱来“模拟”人眼看到的真实情况,当然啦,一个非常牛的算法也可以起到和光线追踪差不多的效果,只不过实现难度可能相差无几
PBR光照:
模拟金属材质的反射和折射是比较困难的,往往有一套专门的算法,网上的资料也很多,一个表现如下(来源于网络):
以上所有的扩展的重心都在:数学,而并非openGL本身