立方体贴图
立方体贴图
在本节中,我们将讨论的是将多个纹理组合起来映射到一张纹理上的一种纹理类型:立方体贴图(Cube Map)。
简单来说,立方体贴图就是一个包含了6个2D纹理的纹理,每个2D纹理都组成了立方体的一个面:一个有纹理的立方体。你可能会奇怪,这样一个立方体有什么用途呢?为什么要把6张纹理合并到一张纹理中,而不是直接使用6个单独的纹理呢?立方体贴图有一个非常有用的特性,它可以通过一个方向向量来进行索引/采样。假设我们有一个1x1x1的单位立方体,方向向量的原点位于它的中心。使用一个橘黄色的方向向量来从立方体贴图上采样一个纹理值会像是这样:
方向向量的大小并不重要,只要提供了方向,OpenGL就会获取方向向量(最终)所击中的纹素,并返回对应的采样纹理值。
如果我们假设将这样的立方体贴图应用到一个立方体上,采样立方体贴图所使用的方向向量将和立方体(插值的)顶点位置非常相像。这样子,只要立方体的中心位于原点,我们就能使用立方体的实际位置向量来对立方体贴图进行采样了。接下来,我们可以将所有顶点的纹理坐标当做是立方体的顶点位置。最终得到的结果就是可以访问立方体贴图上正确面(Face)纹理的一个纹理坐标。
【在立方体贴图中,我们不需要传入一个纹理的坐标采用,可以直接利用传入的位置向量来对传入的立方体贴图进行采样】
如果我们假设将这样的立方体贴图应用到一个立方体上,采样立方体贴图所使用的方向向量将和立方体(插值的)顶点位置非常相像。这样子,只要立方体的中心位于原点,我们就能使用立方体的实际位置向量来对立方体贴图进行采样了。接下来,我们可以将所有顶点的纹理坐标当做是立方体的顶点位置。最终得到的结果就是可以访问立方体贴图上正确面(Face)纹理的一个纹理坐标。
创建立方体贴图
立方体贴图是和其它纹理一样的,所以如果想创建一个立方体贴图的话,我们需要生成一个纹理,并将其绑定到纹理目标上,之后再做其它的纹理操作。这次要绑定到GL_TEXTURE_CUBE_MAP:
unsignedint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
因为立方体贴图包含有6个纹理,每个面一个,我们需要调用glTexImage2D函数6次,参数和之前教程中很类似。但这一次我们将纹理目标(target)参数设置为立方体贴图的一个特定的面,告诉OpenGL我们在对立方体贴图的哪一个面创建纹理。这就意味着我们需要对立方体贴图的每一个面都调用一次glTexImage2D。
由于我们有6个面,OpenGL给我们提供了6个特殊的纹理目标,专门对应立方体贴图的一个面。
纹理目标 |
方位 |
GL_TEXTURE_CUBE_MAP_POSITIVE_X |
右 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_X |
左 |
GL_TEXTURE_CUBE_MAP_POSITIVE_Y |
上 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y |
下 |
GL_TEXTURE_CUBE_MAP_POSITIVE_Z |
后 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z |
前 |
和OpenGL的很多枚举(Enum)一样,它们背后的int值是线性递增的,所以如果我们有一个纹理位置的数组或者vector,我们就可以从GL_TEXTURE_CUBE_MAP_POSITIVE_X开始遍历它们,在每个迭代中对枚举值加1,遍历了整个纹理目标:
【也就是说我们可以以GL_TEXTURE_CUBE_MAP_POSITION_X+i的形式分别对六个面进行传入】;
int width, height, nrChannels;
unsignedchar *data;
for(unsignedint i = 0; i < textures_faces.size(); i++)
{
data = stbi_load(textures_faces[i].c_str(), &width, &height, &nrChannels, 0);
glTexImage2D(
GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data
);//这部分为修改的部分,此函数传入的是立方体贴图的相关数据;
}
这里我们有一个叫做textures_faces的vector,它包含了立方体贴图所需的所有纹理路径,并以表中的顺序排列。这将为当前绑定的立方体贴图中的每个面生成一个纹理。
我们也需要设定它的环绕和过滤方式:
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
//注意这里要设置为GL_CLAMP_TO_EDGE;
GL_TEXTURE_WRAP_R是为纹理的R坐标设置了环绕方式,它对应的是纹理的第三个维度(和位置的z一样)。我们将环绕方式设置为GL_CLAMP_TO_EDGE,这是因为正好处于两个面之间的纹理坐标可能不能击中一个面(由于一些硬件限制),所以通过使用GL_CLAMP_TO_EDGE,OpenGL将在我们对两个面之间采样的时候,永远返回它们的边界值。
【可以试一下,如果采用GL_REPEAT会看到明显的缝隙】;
在绘制使用立方体贴图的物体之前,我们要先激活对应的纹理单元,并绑定立方体贴图,这和普通的2D纹理没什么区别。
在片段着色器中,我们使用了一个不同类型的采样器,samplerCube,我们将使用texture函数使用它进行采样,但这次我们将使用一个vec3的方向向量而不是vec2。使用立方体贴图的片段着色器会像是这样的:
in vec3 textureDir; // 代表3D纹理坐标的方向向量
uniform samplerCube cubemap; // 立方体贴图的纹理采样器voidmain(){
FragColor = texture(cubemap, textureDir);
}
//samplerCube会对立方体贴图的纹理单元进行采样;
看起来很棒,但为什么要用它呢?恰巧有一些很有意思的技术,使用立方体贴图来实现的话会简单多了。其中一个技术就是创建一个天空盒(Skybox)。
天空盒
天空盒是一个包含了整个场景的(大)立方体,它包含周围环境的6个图像,让玩家以为他处在一个比实际大得多的环境当中。游戏中使用天空盒的例子有群山、白云或星空。下面这张截图中展示的是星空的天空盒,它来自于『上古卷轴3』:
立方体贴图能完美满足天空盒的需求:我们有一个6面的立方体,每个面都需要一个纹理。在上面的图片中,他们使用了夜空的几张图片,让玩家产生其位于广袤宇宙中的错觉,但实际上他只是在一个小小的盒子当中。
天空盒图像通常有以下的形式:
如果你将这六个面折成一个立方体,你就会得到一个完全贴图的立方体,模拟一个巨大的场景。一些资源可能会提供了这样格式的天空盒,你必须手动提取六个面的图像,但在大部分情况下它们都是6张单独的纹理图像。
之后我们将在场景中使用这个(高质量的)天空盒,它可以在这里下载到。
加载天空盒
因为天空盒本身就是一个立方体贴图,加载天空盒和之前加载立方体贴图时并没有什么不同。为了加载天空盒,我们将使用下面的函数,它接受一个包含6个纹理路径的vector:
//skybox天空盒的生成函数;//注意返回的是skybox的ID;
unsigned int loadCubemap(std::vector<std::string> faces)//这里传入的是一个包含了六个面的数组;
{
unsigned int textureID;//一个纹理ID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);//此处要绑定的对象是GL_TEXTURE_CUBE_MAP;注意已经不是GL_TEXTURE_2D;
//设置纹理选项;
//此处用上GL_CLAMP_TO_EDGE的原因是因为正好处于两个面之间的纹理坐标可能不能击中一个面(由于一些硬件限制),所以通过使用GL_CLAMP_TO_EDGE,OpenGL将在我们对两个面之间采样的时候,永远返回它们的边界值。
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER,GL_LINEAR);
int width, height, nrchannels;
for (unsigned int i = 0; i < faces.size(); i++)//对六个面分别进行纹理的加载;
{
unsigned char* data = stbi_load(faces[i].c_str(), &width, &height, &nrchannels, 0);//获取对应一个面的纹理数据;
if (data)//如果获取到则进行生成;
{
//对六个面的处理一般为右左上下前后;
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);//生成纹理;
//glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
stbi_image_free(data);//释放数据;
}
else
{
std::cout << "Cubemap texture failed to load at path:" << std::endl;
stbi_image_free(data);
}
}
//结束一系列操作之后,会
return textureID;
}
函数本身应该很熟悉了。它基本就是上一部分中立方体贴图的代码,只不过合并到了一个便于管理的函数中。
之后,在调用这个函数之前,我们需要将合适的纹理路径按照立方体贴图枚举指定的顺序加载到一个vector中。
std::vector<std::string> faces
{
"E:/OpenGl/textures/skybox.texture/skybox/right.jpg",
"E:/OpenGl/textures/skybox.texture/skybox/left.jpg",
"E:/OpenGl/textures/skybox.texture/skybox/top.jpg",
"E:/OpenGl/textures/skybox.texture/skybox/bottom.jpg",
"E:/OpenGl/textures/skybox.texture/skybox/front.jpg",
"E:/OpenGl/textures/skybox.texture/skybox/back.jpg"
};
//直接存入根路径即可;
现在我们就将这个天空盒加载为一个立方体贴图了,它的id是cubemapTexture。
显示天空盒
由于天空盒是绘制在一个立方体上的,和其它物体一样,我们需要另一个VAO、VBO以及新的一组顶点。你可以在这里找到它的顶点数据。
float skyboxVertices[] = {
// positions
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, -1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, -1.0f,
1.0f, 1.0f, -1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f
};
用于贴图3D立方体的立方体贴图可以使用立方体的位置作为纹理坐标来采样。当立方体处于原点(0, 0, 0)时,它的每一个位置向量都是从原点出发的方向向量。这个方向向量正是获取立方体上特定位置的纹理值所需要的。正是因为这个,我们只需要提供位置向量而不用纹理坐标了。
要渲染天空盒的话,我们需要一组新的着色器,它们都不是很复杂。因为我们只有一个顶点属性,顶点着色器非常简单:
#version 330 core
layout (location = 0) in vec3 aPos;
out vec3 TexCoords;
uniform mat4 projection;
uniform mat4 view;
voidmain(){
TexCoords = aPos;
gl_Position = projection * view * vec4(aPos, 1.0);
}
注意,顶点着色器中很有意思的部分是,我们将输入的位置向量作为输出给片段着色器的纹理坐标。片段着色器会将它作为输入来采样samplerCube:
#version 330 core
out vec4 FragColor;
in vec3 TexCoords;
uniform samplerCube skybox;
voidmain(){
FragColor = texture(skybox, TexCoords);
}
片段着色器非常直观。我们将顶点属性的位置向量作为纹理的方向向量,并使用它从立方体贴图中采样纹理值。
有了立方体贴图纹理,渲染天空盒现在就非常简单了,我们只需要绑定立方体贴图纹理,skybox采样器就会自动填充上天空盒立方体贴图了。绘制天空盒时,我们需要将它变为场景中的第一个渲染的物体,并且禁用深度写入。这样子天空盒就会永远被绘制在其它物体的背后了。
glDepthMask(GL_FALSE);
skyboxShader.use();
// ... 设置观察和投影矩阵
glBindVertexArray(skyboxVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
glDepthMask(GL_TRUE);
// ... 绘制剩下的场景
如果你运行一下的话你就会发现出现了一些问题。我们希望天空盒是以玩家为中心的,这样不论玩家移动了多远,天空盒都不会变近,让玩家产生周围环境非常大的印象。然而,当前的观察矩阵【即我们的摄像机移动会改变与其的距离】会旋转、缩放和位移来变换天空盒的所有位置,所以当玩家移动的时候,立方体贴图也会移动!我们希望移除观察矩阵中的位移部分,让移动不会影响天空盒的位置向量。
你可能还记得在基础光照小节中,我们通过取4x4矩阵左上角的3x3矩阵来移除变换矩阵的位移部分。我们可以将观察矩阵转换为3x3矩阵(移除位移),再将其转换回4x4矩阵,来达到类似的效果。
//glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));
//下面是我代码中的内容,我没有把摄像机分装起来;
glm::mat4 view = glm::mat4(glm::mat3(glm::LookAt(cameraPos,cameraFront+cameraPos,cameraUp)));
这将移除任何的位移,但保留旋转变换,让玩家仍然能够环顾场景。
【这里移除了观察矩阵,使得我们不会再次因为移动来改变距离】;
有了天空盒,最终的效果就是一个看起来巨大的场景了。如果你在箱子周围转一转,你就能立刻感受到距离感,极大地提升了场景的真实度。
优化
目前我们是首先渲染天空盒,之后再渲染场景中的其它物体。这样子能够工作,但不是非常高效。如果我们先渲染天空盒,我们就会对屏幕上的每一个像素运行一遍片段着色器,即便只有一小部分的天空盒最终是可见的。可以使用提前深度测试(Early Depth Testing)轻松丢弃掉的片段能够节省我们很多宝贵的带宽。
所以,我们将会最后渲染天空盒,以获得轻微的性能提升。这样子的话,深度缓冲就会填充满所有物体的深度值了,我们只需要在提前深度测试通过的地方渲染天空盒的片段就可以了,很大程度上减少了片段着色器的调用。问题是,天空盒很可能会渲染在所有其他对象之上,因为它只是一个1x1x1的立方体(译注:意味着距离摄像机的距离也只有1),会通过大部分的深度测试。不用深度测试来进行渲染不是解决方案,因为天空盒将会复写场景中的其它物体。我们需要欺骗深度缓冲,让它认为天空盒有着最大的深度值1.0,只要它前面有一个物体,深度测试就会失败。
在坐标系统小节中我们说过,透视除法是在顶点着色器运行之后执行的,将gl_Position的xyz坐标除以w分量。我们又从深度测试小节中知道,相除结果的z分量等于顶点的深度值。使用这些信息,我们可以将输出位置的z分量等于它的w分量,让z分量永远等于1.0,这样子的话,当透视除法执行之后,z分量会变为w / w = 1.0。
voidmain(){
TexCoords = aPos;
vec4 pos = projection * view * vec4(aPos, 1.0);
gl_Position = pos.xyww;
}
最终的标准化设备坐标将永远会有一个等于1.0的z值:最大的深度值。结果就是天空盒只会在没有可见物体的地方渲染了(只有这样才能通过深度测试,其它所有的东西都在天空盒前面)。
我们还要改变一下深度函数,将它从默认的GL_LESS改为GL_LEQUAL。深度缓冲将会填充上天空盒的1.0值,所以我们需要保证天空盒在值小于或等于深度缓冲而不是小于时通过深度测试。
【我们将天空盒的深度值设置为最大的1.0,因此我们只有将深度函数设置为GL_LEQUAL才可以使得小于等于1.0的物体被绘入,如果用的是GL_LESS仅会绘入深度小于1的物体,就没有绘入天空盒;】
深度测试的相关内容可以在这里看到:
https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/01%20Depth%20testing/
以模型的人物为例
代码运行的结果如上图所示;
接下来是反射以及折射的效果:
反射
反射这个属性表现为物体(或物体的一部分)反射它周围环境,即根据观察者的视角,物体的颜色或多或少等于它的环境。镜子就是一个反射性物体:它会根据观察者的视角反射它周围的环境。
下面这张图展示了我们如何计算反射向量,并如何使用这个向量来从立方体贴图中采样:
【联想最开始的图片,这里我们就可以通过反射得到的向量去对天空盒贴图进行采样】;
我们根据观察方向向量I¯I¯和物体的法向量N¯,来计算反射向量R¯。我们可以使用GLSL内建的reflect函数来计算这个反射向量。最终的R¯向量将会作为索引/采样立方体贴图的方向向量,返回环境的颜色值。最终的结果是物体看起来反射了天空盒。
因为我们已经在场景中配置好天空盒了,创建反射效果并不会很难。我们将会改变模型的片段着色器,让其有反射性:
#version 330 core
out vec4 FragColor;
#define Max_texture_size 8
struct Material {
//这里创建两个纹理单元;
sampler2D texture_diffuse[Max_texture_size];
sampler2D texture_specular[Max_texture_size];
int texture_diffuse_num;
int texture_specular_num;//这两个参数用于记录两种纹理的最终个数,以便用于调用纹理单元;
float shininess;
//---------
samplerCube skybox;//设置一个采样器;
//---------
};
//定向光的结构体;
struct DirLight{
vec3 direction;//定向光的方向;
vec3 ambient;
vec3 diffuse;
vec3 specular;//定向光的三种分量;
};
//点光源的结构体;
struct PointLight{
vec3 position;//点光源不用考虑光照方向;光照方向即为与片段的连线;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
//聚光的结构体;
struct SpotLight{
vec3 position;
vec3 direction;
float cutOff;
float outerCutOff;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
//这里我设置了两个光源;
#define NR_POINT_LIGHTS 2
in vec3 FragPos; //片段的插值的Normal以及FragColor;
in vec3 Normal;
in vec2 TexCoords;
uniform vec3 viewPos;
uniform Material material;
uniform DirLight dirlight;//声明一个定向光结构体的变量;
uniform PointLight pointlights[NR_POINT_LIGHTS];//创建一个包含2个元素的PointLight结构体数组;
uniform SpotLight spotlight;//声明一个聚光的结构体变量;
vec3 CalcDirLight(DirLight light,vec3 normal,vec3 viewDir);//定向光;
vec3 CalcPointLight(PointLight light,vec3 normal,vec3 fragPos,vec3 viewDir);//点光源;
vec3 CalcSpotLight(SpotLight light,vec3 normal,vec3 fragPos,vec3 viewDir);//聚光;
vec3 CalLightSum(vec3 lightAmbient, vec3 lightDiffuse, vec3 lightSpecular, float diff, float spec);//用于计算最终片段的颜色数值的函数;
//change;
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);
//实现反射;
//---------------------
vec3 I = normalize(FragPos - viewPos);
vec3 R = reflect(I,normalize(Normal));
result += texture(material.skybox,R).rgb;
//---------------------
//实现折射;运用rafract函数;
//float ratio = 1.0/1.52;
//vec3 I = normalize(FragPos - viewPos);
//vec3 R = refract(I,normalize(Normal),ratio);
//result += texture(material.skybox,R).rgb;
//输出最终片段;
FragColor = vec4(result,1.0);//最终片段的颜色是三种光照的相加值;
}
//change;
//一个用于计算定向光最后的颜色函数;(返回的是一个vec3的向量);
vec3 CalcDirLight(DirLight light,vec3 normal,vec3 viewDir)//函数传入的是定向光的结构体,以及片段已经单位化的法线,还有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);
//进行合并;
return CalLightSum(light.ambient, light.diffuse, light.specular, diff, spec);;
}
//change;
//一个用于计算点光源最终输出颜色的函数;
vec3 CalcPointLight(PointLight light,vec3 normal,vec3 fragPos,vec3 viewDir)//normal已经单位化;
{
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));
return CalLightSum(light.ambient, light.diffuse, light.specular, diff, spec)*attenuation;
}
//change;
//一个用于计算聚光最终输出的函数;
//传入聚光结构体数组,单位化后的法线,片段位置,以及viewDir视角方向;
vec3 CalcSpotLight(SpotLight light,vec3 normal,vec3 fragPos,vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);//光线方向;
//漫反射着色;
float diff = max(0.0,dot(lightDir,normal));
//镜面光着色;
vec3 reflectDir = reflect(-lightDir,normal);
float spec = pow(max(0.0,dot(reflectDir,viewDir)),material.shininess);
//衰减;
float distance = length(light.position - fragPos);
float atten = 1.0 / (light.constant+distance * light.linear+light.quadratic*(distance*distance));
//柔和;
float theta = dot(lightDir,normalize(-light.direction));
float epison = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epison,0.0,1.0);//利用clamp将柔和限制在0到1之间;
//进行整合;
return CalLightSum(light.ambient, light.diffuse, light.specular, diff, spec)*atten*intensity;
}
//创建一个用来计算漫反射,环境光,以及高光三者颜色综合的一个函数;
vec3 CalLightSum(vec3 lightAmbient, vec3 lightDiffuse, vec3 lightSpecular, float diff, float spec)
{
vec3 diffuse = vec3(0.0,0.0,0.0);
for(int i = 0; i < material.texture_diffuse_num; i++)
{
diffuse += texture(material.texture_diffuse[i], TexCoords).rgb;
}
vec3 ambient = lightAmbient * diffuse;
diffuse = lightDiffuse * diff * diffuse;
vec3 specular = vec3(0.0,0.0,0.0);
for(int i = 0; i < material.texture_specular_num; i++)
{
specular += texture(material.texture_specular[i], TexCoords).rgb;
}
specular = lightSpecular * spec * specular;
return ambient + diffuse + specular;
}
下面是顶点着色器:
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoords;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
out vec3 Normal;
out vec3 FragPos;//输出一个通过model矩阵转换到世界空间的顶点;
out vec2 TexCoords;
void main()
{
gl_Position = projection*view*model*vec4(aPos,1.0);
FragPos = vec3(model*vec4(aPos,1.0f));//片段的位置经过模型空间的变化到世界空间;
Normal = mat3(transpose(inverse(model)))*aNormal;
TexCoords = aTexCoords;//传入纹理坐标到片段着色器;
}
我们现在使用了一个法向量,所以我们将再次使用法线矩阵(Normal Matrix)来变换它们。FragPos输出向量是一个世界空间的位置向量。【仅用了model变换而没有用view】顶点着色器的这个Position输出将用来在片段着色器内计算观察方向向量。
在main.cpp中还要进行绑定;
// render the loaded model
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f));
model = glm::scale(model, glm::vec3(1.0f, 1.0f, 1.0f)); // it's a bit too big for our scene, so scale it down
lightshader.setMat4("model", model);
//------------------
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
//------------------
OurModel.Draw(lightshader);//调用Draw进行绘制相关操作;
结果:
折射
环境映射的另一种形式是折射,它和反射很相似。折射是光线由于传播介质的改变而产生的方向变化。在常见的类水表面上所产生的现象就是折射,光线不是直直地传播,而是弯曲了一点。将你的半只胳膊伸进水里,观察出来的就是这种效果。
折射是通过斯涅尔定律(Snell’s Law)来描述的,使用环境贴图的话看起来像是这样:
同样,我们有一个观察向量I¯,一个法向量N¯,而这次是折射向量R¯。可以看到,观察向量的方向轻微弯曲了。弯折后的向量R¯将会用来从立方体贴图中采样。
折射可以使用GLSL的内建refract函数来轻松实现,它需要一个法向量、一个观察方向和两个材质之间的折射率(Refractive Index)。
折射率决定了材质中光线弯曲的程度,每个材质都有自己的折射率。一些最常见的折射率可以在下表中找到:
材质 |
折射率 |
空气 |
1.00 |
水 |
1.33 |
冰 |
1.309 |
玻璃 |
1.52 |
钻石 |
2.42 |
我们使用这些折射率来计算光传播的两种材质间的比值。在我们的例子中,光线/视线从空气进入玻璃(如果我们假设模型是玻璃制的),所以比值为1.00/1.52=0.658。
我们已经绑定了立方体贴图,提供了顶点数据和法线,并设置了摄像机位置的uniform。唯一要修改的就是片段着色器:
//........
for(int i = 0; i < NR_POINT_LIGHTS; i++)
result += CalcPointLight(pointlights[i],norm, FragPos, viewDir);
//计算聚光;
result += CalcSpotLight(spotlight, norm, FragPos, viewDir);
//实现反射;
//---------------------
//vec3 I = normalize(FragPos - viewPos);
//vec3 R = reflect(I,normalize(Normal));
//result += texture(material.skybox,R).rgb;
//---------------------
//实现折射;运用rafract函数;
float ratio = 1.0/1.52;
vec3 I = normalize(FragPos - viewPos);
vec3 R = refract(I,normalize(Normal),ratio);
result += texture(material.skybox,R).rgb;
//.......
通过改变折射率,你可以创建完全不同的视觉效果。编译程序并运行,我们可以看到一个类玻璃的物体:
好看捏!!;
下面是main.cpp和片段着色器的代码:
main.cpp
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include<E:\OpenGl\练习1.1\3.3.shader_class\shader s.h>//引入shader类头文件;
//以下三行为glm的头文件代码;
#include <E:\OpenGl\glm\glm-master\glm\glm.hpp>
#include <E:\OpenGl\glm\glm-master\glm\gtc\matrix_transform.hpp>
#include <E:\OpenGl\glm\glm-master\glm\gtc\type_ptr.hpp>
//#define STB_IMAGE_IMPLEMENTATION
#include <E:/OpenGl/stb_image.h/stb-master/stb_image.h>//这两行代码加入了stb_image库;
#include"Model s.h"
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_back(GLFWwindow* window, double xoffset, double yoffset);
unsigned int loadCubemap(std::vector<std::string> faces);//传入六张图片,返回一个纹理ID;
//三个调整摄像机位置的全局变量;
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
float deltatime = 0.0f;//上一帧与这一帧的时间差;
float lastime = 0.0f;//上一帧的时间;
//用来存储上一帧鼠标的位置!,设置为屏幕中心;
float lastX = 400.0;
float lastY = 300.0;
//仰俯角和偏航角;
float pitch = 0.0f;
float yaw = -90.0f;//从下往上;
float fov = 45.0f;//视域角;
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);//声明一个光源,表示光源在空间中的位置;
int main()
{
//先进行初始化glfw;
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本设置为3;
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本设置为3;
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(1000, 750, "MY OPENGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Fail to create a window" << std::endl;
glfwTerminate();//释放资源;
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
//创建完告诉将上下文设置为进程上下文;
//以下两步用于摄像机操作中的设置,由于是窗口的操作,因此放在此处!!;
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);//告诉GLFW隐藏光标并捕捉他;
glfwSetCursorPosCallback(window, mouse_callback);//此句代码当发生鼠标移动时,就会调用mouse_callback函数改变两个欧拉角,
//进而改变cameraFront方向向量,进而可以实现3D旋转;
//还要对视域角fov做出变化,可以进行放大缩小;
glfwSetScrollCallback(window, scroll_back);//当滚动鼠标滚轮的时候,我们就可以通过调用该函数来改变fov,进而改变透视投影矩阵,
//以此来进一步形成放大和缩小!!;
//对glad进行初始化;
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Fail to initnite glad" << std::endl;
return -1;
}
//引入着色器类,着色器被封装到了class Shader里面;
//这里要创建两个着色器程序,分别用在物体和光源上面;
//
// light positions
float vertices[] = {
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, -0.5f,
};
glEnable(GL_DEPTH_TEST);//启用深度测试;
//光源
unsigned int lightVAO;
unsigned int lightVBO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
glGenBuffers(1, &lightVBO);
glBindBuffer(GL_ARRAY_BUFFER, lightVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
//天空盒;
//天空盒立方体顶点数据;
float skyboxVertices[] = {
// positions
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, -1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, -1.0f,
1.0f, 1.0f, -1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f
};
//创建天空盒的VAO,VBO;
//skybox;VAO,VBO;
//此处创建一个新的VAO,VBO用于存储顶点数据;
unsigned int skyboxVAO, skyboxVBO;
glGenVertexArrays(1, &skyboxVAO);
glGenBuffers(1, &skyboxVBO);
glBindVertexArray(skyboxVAO);
glBindBuffer(GL_ARRAY_BUFFER, skyboxVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(skyboxVertices), &skyboxVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
//ready to be change;
std::vector<std::string> faces
{
"E:/OpenGl/textures/skybox.texture/skybox/right.jpg",
"E:/OpenGl/textures/skybox.texture/skybox/left.jpg",
"E:/OpenGl/textures/skybox.texture/skybox/top.jpg",
"E:/OpenGl/textures/skybox.texture/skybox/bottom.jpg",
"E:/OpenGl/textures/skybox.texture/skybox/front.jpg",
"E:/OpenGl/textures/skybox.texture/skybox/back.jpg"
};
unsigned int cubemapTexture = loadCubemap(faces);//获取到天空盒的ID;
//下面要传入两个天空盒的着色器进行着色绘制;
Shader lightshader("3.2.shader2.vs", "3.2.shader2.fs");
Shader lightCubeshader("3.2.shader.light.vs", "3.2.shader.light.fs");//用于光源的着色器程序;//后面会绘制两个光源;
Shader skyboxshader("4.4.sky.shader.vs", "4.4.sky.shader.fs");//用于绘制天空盒的着色器程序;
skyboxshader.useProgram();
skyboxshader.setInt("skybox", 0);
//---------------------------
lightshader.useProgram();
lightshader.setInt("material.skybox", 3);
//---------------------------
//模型导入:
Model OurModel("E:/OpenGl/Model/Model1/nanosuit.obj");
//光源位置;
glm::vec3 pointLightPositions[] = {
glm::vec3(-2.0f,6.0f,1.0f),
glm::vec3(2.0f,15.0f,0.0f)
};
//准备引擎:
while (!glfwWindowShouldClose(window))
{
float currentFrame = static_cast<float>(glfwGetTime());
deltatime = currentFrame - lastime;
lastime = currentFrame;
processInput(window);
glClearColor(0.05f, 0.05f, 0.05f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// don't forget to enable shader before setting uniforms
lightshader.useProgram();//启用着色器;
lightshader.setFloat("material.shininess", 32.0f);//设置material中的选项;
//人眼设置;//这里设置为摄像机的位置;
lightshader.setVec3("viewPos", cameraPos);
//定向光参数设置;
lightshader.setVec3("dirlight.position", glm::vec3(-2.0f, -0.3f, -1.0f));
lightshader.setVec3("dirlight.ambient", glm::vec3(0.2f, 0.2f, 0.2f));
lightshader.setVec3("dirlight.specular", glm::vec3(1.0f, 1.0f, 1.0f));
lightshader.setVec3("dirlight.diffuse", glm::vec3(0.5f));
//点光源参数设置;下面有两个电光源需要进行设置;
//point1;
lightshader.setVec3("pointlights[0].position", pointLightPositions[0]);
lightshader.setVec3("pointlights[0].ambient", glm::vec3(0.2f, 0.2f, 0.2f));
lightshader.setVec3("pointlights[0].specular", glm::vec3(50.0f, 50.0f, 50.0f));
lightshader.setVec3("pointlights[0].diffuse", glm::vec3(0.5f, 0.5f, 0.5f));
lightshader.setFloat("pointlights[0].constant", 1.0f);
lightshader.setFloat("pointlights[0].linear", 0.09f);
lightshader.setFloat("pointlights[0].quadratic", 0.032f);
//point2;
lightshader.setVec3("pointlights[1].position", pointLightPositions[1]);
lightshader.setVec3("pointlights[1].ambient", glm::vec3(0.2f, 0.2f, 0.2f));
lightshader.setVec3("pointlights[1].specular", glm::vec3(10.0f, 10.0f, 10.0f));
lightshader.setVec3("pointlights[1].diffuse", glm::vec3(0.5f, 0.5f, 0.5f));
lightshader.setFloat("pointlights[1].constant", 1.0f);
lightshader.setFloat("pointlights[1].linear", 0.09f);
lightshader.setFloat("pointlights[1].quadratic", 0.032f);
//聚光参数设置;
lightshader.setVec3("spotlight.position", cameraPos);
lightshader.setVec3("spotlight.direction", cameraFront);
lightshader.setVec3("spotlight.ambient", glm::vec3(0.2f, 0.2f, 0.2f));
lightshader.setVec3("spotlight.specular", glm::vec3(1.0f, 1.0f, 1.0f));
lightshader.setVec3("spotlight.diffuse", glm::vec3(0.5f, 0.5f, 0.5f));
lightshader.setFloat("spotlight.constant", 1.0f);
lightshader.setFloat("spotlight.linear", 0.09f);
lightshader.setFloat("spotlight.quadratic", 0.032f);
lightshader.setFloat("spotlight.outOff", glm::radians(12.5f));
lightshader.setFloat("spotlight.outerCutOff", glm::radians(17.5f));
// view/projection transformations
glm::mat4 projection = glm::perspective(glm::radians(fov), (float)800 / (float)600, 0.1f, 100.0f);
glm::mat4 view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
lightshader.setMat4("projection", projection);
lightshader.setMat4("view", view);
// render the loaded model
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f));
model = glm::scale(model, glm::vec3(1.0f, 1.0f, 1.0f)); // it's a bit too big for our scene, so scale it down
lightshader.setMat4("model", model);
//------------------
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
//------------------
OurModel.Draw(lightshader);//调用Draw进行绘制相关操作;
//下面是对灯源的操作,会在场景中画出两个灯光位置,灯光均为白色;
lightCubeshader.useProgram();//启用另外一个着色器;
glBindVertexArray(lightVAO);
lightCubeshader.setMat4("view", view);
lightCubeshader.setMat4("projection", projection);
for (unsigned int i = 0; i < 2; i++)
{
model = glm::translate(glm::mat4(1.0f), pointLightPositions[i]);
model = glm::rotate(model, glm::radians(180.0f), glm::vec3(0.0f, 1.0f, 0.0f));
model = glm::scale(model, glm::vec3(0.25f, 0.25f, 0.25f));
lightCubeshader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
// 最后画天空盒
//更改深度函数,以便深度测试在值等于深度缓冲区的内容时通过
//注意此处深度测试是重点!!!!;
glDepthFunc(GL_LEQUAL);
skyboxshader.useProgram();
view = glm::mat4(glm::mat3(view)); //从视图矩阵中删除平移
skyboxshader.setMat4("view", view);
skyboxshader.setMat4("projection", projection);
// 天空盒
glBindVertexArray(skyboxVAO);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
glDepthFunc(GL_LESS); // 将深度功能设置回默认值
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//如果按下的键为回车键;
glfwSetWindowShouldClose(window, true);
float cameraSpeed = 10.0f * deltatime;//移动速度;
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
cameraPos += cameraUp * cameraSpeed;
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
cameraPos -= cameraUp * cameraSpeed;
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
cameraPos -= cameraSpeed * glm::normalize(glm::cross(cameraFront, cameraUp));
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
cameraPos += cameraSpeed * glm::normalize(glm::cross(cameraFront, cameraUp));
}
bool firstMouse = true;
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
//计算鼠标距上一帧的偏移量。
//把偏移量添加到摄像机的俯仰角和偏航角中。
//对偏航角和俯仰角进行最大和最小值的限制。
//计算方向向量。
if (firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;//否则每一次都会进行循环;
}
//1.计算鼠标距上一帧的偏移量。
float xoffset = xpos - lastX;
float yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;//更新存储的上一帧的值;
float sensitivity = 0.1f;//设置灵敏度;
xoffset *= sensitivity;
yoffset *= sensitivity;
//2.把偏移量添加到摄像机的俯仰角和偏航角中。
pitch = pitch + yoffset;
yaw = yaw + xoffset;
//3.对偏航角和俯仰角进行最大和最小值的限制
if (pitch > 89.0f)
pitch = 89.0f;
if (pitch < -89.0f)
pitch = -89.0f;
//计算方向向量。
glm::vec3 direction;
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
cameraFront = glm::normalize(direction);
}
void scroll_back(GLFWwindow* window, double xoffset, double yoffset)
{
//我们要把fov限制在1.0到45.0之间!!;
if (fov >= 1.0f && fov <= 45.0f)
{
fov -= yoffset;
}
if (fov >= 45.0f)
{
fov = 45.0f;
}
if (fov <= 1.0f)
{
fov = 1.0f;
}
}
//skybox天空盒的生成函数;//注意返回的是skybox的ID;
unsigned int loadCubemap(std::vector<std::string> faces)//这里传入的是一个包含了六个面的数组;
{
unsigned int textureID;//一个纹理ID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);//此处要绑定的对象是GL_TEXTURE_CUBE_MAP;注意已经不是GL_TEXTURE_2D;
//设置纹理选项;
//此处用上GL_CLAMP_TO_EDGE的原因是因为正好处于两个面之间的纹理坐标可能不能击中一个面(由于一些硬件限制),所以通过使用GL_CLAMP_TO_EDGE,OpenGL将在我们对两个面之间采样的时候,永远返回它们的边界值。
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER,GL_LINEAR);
int width, height, nrchannels;
for (unsigned int i = 0; i < faces.size(); i++)//对六个面分别进行纹理的加载;
{
unsigned char* data = stbi_load(faces[i].c_str(), &width, &height, &nrchannels, 0);//获取对应一个面的纹理数据;
if (data)//如果获取到则进行生成;
{
//对六个面的处理一般为右左上下前后;
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);//生成纹理;
//glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
stbi_image_free(data);//释放数据;
}
else
{
std::cout << "Cubemap texture failed to load at path:" << std::endl;
stbi_image_free(data);
}
}
//结束一系列操作之后,会
return textureID;
}
片段着色器
#version 330 core
out vec4 FragColor;
#define Max_texture_size 8
struct Material {
//这里创建两个纹理单元;
sampler2D texture_diffuse[Max_texture_size];
sampler2D texture_specular[Max_texture_size];
int texture_diffuse_num;
int texture_specular_num;//这两个参数用于记录两种纹理的最终个数,以便用于调用纹理单元;
float shininess;
//---------
samplerCube skybox;//设置一个采样器;
//---------
};
//定向光的结构体;
struct DirLight{
vec3 direction;//定向光的方向;
vec3 ambient;
vec3 diffuse;
vec3 specular;//定向光的三种分量;
};
//点光源的结构体;
struct PointLight{
vec3 position;//点光源不用考虑光照方向;光照方向即为与片段的连线;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
//聚光的结构体;
struct SpotLight{
vec3 position;
vec3 direction;
float cutOff;
float outerCutOff;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
//这里我设置了两个光源;
#define NR_POINT_LIGHTS 2
in vec3 FragPos; //片段的插值的Normal以及FragColor;
in vec3 Normal;
in vec2 TexCoords;
uniform vec3 viewPos;
uniform Material material;
uniform DirLight dirlight;//声明一个定向光结构体的变量;
uniform PointLight pointlights[NR_POINT_LIGHTS];//创建一个包含2个元素的PointLight结构体数组;
uniform SpotLight spotlight;//声明一个聚光的结构体变量;
vec3 CalcDirLight(DirLight light,vec3 normal,vec3 viewDir);//定向光;
vec3 CalcPointLight(PointLight light,vec3 normal,vec3 fragPos,vec3 viewDir);//点光源;
vec3 CalcSpotLight(SpotLight light,vec3 normal,vec3 fragPos,vec3 viewDir);//聚光;
vec3 CalLightSum(vec3 lightAmbient, vec3 lightDiffuse, vec3 lightSpecular, float diff, float spec);//用于计算最终片段的颜色数值的函数;
//change;
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);
//实现反射;
//---------------------
//vec3 I = normalize(FragPos - viewPos);
//vec3 R = reflect(I,normalize(Normal));
//result += texture(material.skybox,R).rgb;
//---------------------
//实现折射;运用rafract函数;
float ratio = 1.0/1.52;
vec3 I = normalize(FragPos - viewPos);
vec3 R = refract(I,normalize(Normal),ratio);
result += texture(material.skybox,R).rgb;
//输出最终片段;
FragColor = vec4(result,1.0);//最终片段的颜色是三种光照的相加值;
}
//change;
//一个用于计算定向光最后的颜色函数;(返回的是一个vec3的向量);
vec3 CalcDirLight(DirLight light,vec3 normal,vec3 viewDir)//函数传入的是定向光的结构体,以及片段已经单位化的法线,还有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);
//进行合并;
return CalLightSum(light.ambient, light.diffuse, light.specular, diff, spec);;
}
//change;
//一个用于计算点光源最终输出颜色的函数;
vec3 CalcPointLight(PointLight light,vec3 normal,vec3 fragPos,vec3 viewDir)//normal已经单位化;
{
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));
return CalLightSum(light.ambient, light.diffuse, light.specular, diff, spec)*attenuation;
}
//change;
//一个用于计算聚光最终输出的函数;
//传入聚光结构体数组,单位化后的法线,片段位置,以及viewDir视角方向;
vec3 CalcSpotLight(SpotLight light,vec3 normal,vec3 fragPos,vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);//光线方向;
//漫反射着色;
float diff = max(0.0,dot(lightDir,normal));
//镜面光着色;
vec3 reflectDir = reflect(-lightDir,normal);
float spec = pow(max(0.0,dot(reflectDir,viewDir)),material.shininess);
//衰减;
float distance = length(light.position - fragPos);
float atten = 1.0 / (light.constant+distance * light.linear+light.quadratic*(distance*distance));
//柔和;
float theta = dot(lightDir,normalize(-light.direction));
float epison = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epison,0.0,1.0);//利用clamp将柔和限制在0到1之间;
//进行整合;
return CalLightSum(light.ambient, light.diffuse, light.specular, diff, spec)*atten*intensity;
}
//创建一个用来计算漫反射,环境光,以及高光三者颜色综合的一个函数;
vec3 CalLightSum(vec3 lightAmbient, vec3 lightDiffuse, vec3 lightSpecular, float diff, float spec)
{
vec3 diffuse = vec3(0.0,0.0,0.0);
for(int i = 0; i < material.texture_diffuse_num; i++)
{
diffuse += texture(material.texture_diffuse[i], TexCoords).rgb;
}
vec3 ambient = lightAmbient * diffuse;
diffuse = lightDiffuse * diff * diffuse;
vec3 specular = vec3(0.0,0.0,0.0);
for(int i = 0; i < material.texture_specular_num; i++)
{
specular += texture(material.texture_specular[i], TexCoords).rgb;
}
specular = lightSpecular * spec * specular;
return ambient + diffuse + specular;
}
练习
尝试在我们之前在模型加载小节中创建的模型加载器中引入反射贴图。你可以在这里找到升级后有反射贴图的纳米装模型。仍有几点要注意的:
Assimp在大多数格式中都不太喜欢反射贴图,所以我们需要欺骗一下它,将反射贴图储存为漫反射贴图。你可以在加载材质的时候将反射贴图的纹理类型设置为aiTextureType_AMBIENT。
由于模型加载器本身就已经在着色器中占用了3个纹理单元了,你需要将天空盒绑定到第4个纹理单元上,因为我们要从同一个着色器中对天空盒采样。
修改代码如下:
struct Material {
sampler2D texture_diffuse1;
sampler2D texture_specular1;
sampler2D texture_reflection1;
samplerCube texture1;
sampler2D texture_normal1;
sampler2D texture_height1;
float shininess;
};
然后在计算反射颜色时将反射贴图颜色和天空盒颜色相乘,加在结果上
vec3 R = reflect(- viewDir, norm);
vec3 reflectMap = vec3(texture(material.texture_reflection1, TexCoords));
vec3 reflection = vec3(texture(material.texture1, R).rgb) * reflectMap * 2;
那么读取的时候怎么加载这个纹理呢?
Assimp在大多数格式中都不太喜欢反射贴图,所以我们需要欺骗一下它,将反射贴图储存为漫反射贴图。你可以在加载材质的时候在model.h里面将反射贴图的纹理类型设置为aiTextureType_AMBIENT,加入textures
// 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());
// 3. normal maps
std::vector<Texture> normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal");
textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());
// 4. height maps
std::vector<Texture> heightMaps = loadMaterialTextures(material, aiTextureType_AMBIENT, "texture_height");
textures.insert(textures.end(), heightMaps.begin(), heightMaps.end());
// 5. reflection maps
std::vector<Texture> reflectionMaps = loadMaterialTextures(material, aiTextureType_AMBIENT, "texture_reflection");
textures.insert(textures.end(), reflectionMaps.begin(), reflectionMaps.end());
然后在mesh.h里面激活绑定反射贴图和天空盒贴图,由于模型加载器本身就已经在着色器中占用了3个纹理单元了,我们需要将天空盒绑定到第4个纹理单元上,因为我们要从同一个着色器中对天空盒采样。
shader.setInt("material.texture1", 3);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_CUBE_MAP, texture);
if (name == "texture_diffuse")
number = std::to_string(diffuseNr++);
else if (name == "texture_specular")
number = std::to_string(specularNr++); // transfer unsigned int to string
else if (name == "texture_normal")
number = std::to_string(normalNr++); // transfer unsigned int to string
else if (name == "texture_height")
number = std::to_string(heightNr++); // transfer unsigned int to string
else if (name == "texture_reflection")
number = std::to_string(reflectionNr++); // transfer unsigned int to string
// now set the sampler to the correct texture unit
shader.setInt(("material." + name + number).c_str(), i);
// and finally bind the texture
glBindTexture(GL_TEXTURE_2D, textures[i].id);
结果: