序言
以glew、glfw库
OpenGL学习网站
glfw官网
OpenGL-API文档
glew官网
纹理
纹理:是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节。
以上图为例,为了能够把纹理映射(Map)
到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate)
,用来标明该从纹理图像的哪个部分采样(译注:采集片段颜色)
。之后在图形的其它片段上进行片段插值(Fragment Interpolation)
。
纹理坐标
在x
和y
轴上,范围为0
到1
之间(注意我们使用的是2D纹理图像
)。使用纹理坐标
获取纹理颜色叫做采样(Sampling)
。纹理坐标
起始于(0, 0)
,也就是纹理图片的左下角
,终始于(1, 1)
,即纹理图片的右上角
。
下面的图片展示了我们是如何把纹理坐标映射到三角形上的。
我们为三角形指定了3个纹理坐标点。如上图所示,我们希望三角形的左下角对应纹理的左下角,因此我们把三角形左下角顶点的纹理坐标设置为(0, 0)
;三角形的上顶点对应于图片的上中位置所以我们把它的纹理坐标设置为(0.5, 1.0)
;同理右下方的顶点设置为(1, 0)
。我们只要给顶点着色器传递这三个纹理坐标就行了,接下来它们会被传片段着色器中,它会为每个片段进行纹理坐标
的插值
。
纹理坐标看起来就像这样:
GLfloat texCoords[] = {
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
0.5f, 1.0f // 上中
};
对纹理采样的解释非常宽松,它可以采用几种不同的插值方式。所以我们需要自己告诉OpenGL该怎样对纹理采样。
纹理环绕方式
纹理坐标
的范围通常是从(0, 0)
到(1, 1)
,那如果我们把纹理坐标设置在范围之外会发生什么?OpenGL默认的行为是重复这个纹理图像(我们基本上忽略浮点纹理坐标的整数部分),但OpenGL提供了更多的选择:
环绕方式(Wrapping) | 描述 |
---|---|
GL_REPEAT | 对纹理的默认行为。重复纹理图像。 |
GL_MIRRORED_REPEAT | 和GL_REPEAT 一样,但每次重复图片是镜像放置的。 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0 到1 之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色。 |
当纹理坐标超出默认范围时,每个选项都有不同的视觉效果输出。下面是示例:
可使用glTexParameter*
函数对环绕方式
对单独的坐标轴
设置(s
、t
(如果是使用3D纹理那么还有一个r
)它们和x
、y
、z
是等价的):
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT); //GL_TEXTURE_WRAP_S是S轴,对应X轴
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT); //GL_TEXTURE_WRAP_T是T轴,对应Y轴
glTexParameter*:设置纹理的坐标轴。
- 第一个参数:指定了纹理目标。示例是2D纹理,因此纹理目标是
GL_TEXTURE_2D
。 - 第二个参数:需要我们指定设置的选项与应用的
纹理轴
。我们配置的是WRAP选项,并且指定S
和T
轴。 - 第三个参数:传递环绕方式。
如果我们选择GL_CLAMP_TO_BORDER
选项,我们还需要指定一个边缘的颜色。这需要使用glTexParameter
函数的fv
后缀形式,用GL_TEXTURE_BORDER_COLOR
作为它的选项,并且传递一个float
数组作为边缘的颜色值:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
纹理过滤
纹理坐标
不依赖于分辨率(Resolution)
,它可以是任意浮点值,所以OpenGL需要知道怎样将纹理像素
映射到纹理坐标
。当你有一个很大的物体但是纹理的分辨率很低的时候这就变得很重要了。
纹理过滤有很多个选项,但是现在我们只讨论最重要的两种:GL_NEAREST
和GL_LINEAR
。
Texture Pixel
也叫Texel
,你可以想象你打开一张.jpg
格式图片,不断放大你会发现它是由无数像素点组成的,这个点就是纹理像素;注意不要和纹理坐标搞混,纹理坐标是你给模型顶点设置的那个数组,OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素,然后进行采样提取纹理像素的颜色。
临近过滤
GL_NEAREST
:也叫临近过滤
(Nearest Neighbor Filtering)。是OpenGL默认的纹理过滤方式。
当设置为GL_NEAREST
的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。
下图中你可以看到四个像素,加号代表纹理坐标
。左上角那个纹理像素
的中心距离纹理坐标
最近,所以它会被选择为样本颜色:
线性过滤
GL_LINEAR
:也叫线性过滤
((Bi)linear Filtering)。
它会基于纹理坐标附近的纹理像素
,计算出一个插值,近似出这些纹理像素之间的颜色。
一个纹理像素
的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。
下图中你可以看到返回的颜色是邻近像素的混合色:
那么两种过滤效果怎样呢?我们看看在一个很大的物体上应用一张低分辨率的纹理会发生什么吧(纹理被放大了,每个纹理像素都能看到):
GL_NEAREST
产生了颗粒状的图案,我们能够清晰看到组成纹理的像素,而GL_LINEAR
能够产生更平滑的图案,很难看出单个的纹理像素。GL_LINEAR
可以产生更真实的输出,但有些开发者更喜欢8-bit
风格,所以他们会用GL_NEAREST
选项。
有些图形处理后会出现锯齿状,可以通过GL_LINEAR
处理。
当进行放大(Magnify)
和缩小(Minify)
操作的时候可以设置纹理过滤的选项,比如你可以在纹理被缩小的时候使用邻近过滤
,被放大时使用线性过滤
。我们需要使用glTexParameter*
函数为放大和缩小指定过滤方式。这段代码看起来会和纹理环绕方式的设置很相似:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); //缩小时采用临近过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //放大时采用线性过滤
多级渐远纹理
想象一下,假设我们有一个包含着上千物体的大房间,每个物体上都有纹理。有些物体会很远,但其纹理会拥有与近处物体同样高的分辨率。由于远处的物体可能只产生很少的片段,OpenGL从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物体上这会产生不真实的感觉,更不用说对它们使用高分辨率纹理浪费内存的问题了。
主要用于缩小效果,如同其名字。距离越远,物体越小。
OpenGL使用一种叫做多级渐远纹理(Mipmap)
的概念来解决这个问题,它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。
多级渐远纹理
背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。让我们看一下多级渐远纹理是什么样子的:
OpenGL有一个glGenerateMipmaps
函数,在创建完一个纹理后调用它OpenGL就会承担接下来的所有工作了。
在渲染中切换多级渐远纹理级别(Level)
时,OpenGL在两个不同级别的多级渐远纹理层
之间会产生不真实的生硬边界。就像普通的纹理过滤一样,切换多级渐远纹理级别时你也可以在两个不同多级渐远纹理级别之间使用NEAREST(临近)
和LINEAR(线性)
过滤。
为了指定不同多级渐远纹理级别之间的过滤方式,你可以使用下面四个选项中的一个代替原有的过滤方式:
过滤方式 | 描述 |
---|---|
GL_NEAREST_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理 来匹配像素大小,并使用邻近插值 进行纹理采样 |
GL_LINEAR_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理 级别,并使用线性插值 进行采样 |
GL_NEAREST_MIPMAP_LINEAR | 在两个最匹配像素大小的多级渐远纹理 之间进行线性插值,使用邻近插值 进行采样 |
GL_LINEAR_MIPMAP_LINEAR | 在两个邻近的多级渐远纹理之间使用线性插值 ,并使用线性插值 进行采样 |
就像纹理过滤一样,我们可以使用glTexParameteri
将过滤方式设置为前面四种提到的方法之一:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); //多级渐远纹理主要用于缩小,放大无效果
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
一个常见的错误是,将放大过滤的选项设置为多级渐远纹理过滤选项之一。这样没有任何效果,因为多级渐远纹理主要是使用在纹理被缩小的情况下的:纹理放大不会使用多级渐远纹理,为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM
错误代码。
加载与创建纹理
用于将图像加载到我们应用中。即图像加载库(图像库)。这里以SOIL2
为例(SOIL
太老了)。
SOIL2
SOIL2是简易OpenGL图像库(Simple OpenGL Image Library2)的缩写,它支持大多数流行的图像格式,使用起来也很简单。
将soil2
编译出lib
库,可使用premake
工具进行。
将导出lib
库和SOIL2.h
按照之前配置的OpenGL配置即可。
OpenGL环境搭建(GLEW版)
使用SOIL2
加载图片,我们需要使用它的SOIL_load_image
函数:
int width, height;
unsigned char* image = SOIL_load_image("container.jpg", &width, &height, 0, SOIL_LOAD_RGB);
SOIL_load_image
:记载图片。
- 第一个参数:图片地址。
- 第二个参数:图片宽和高,
SOIL2
会返回宽和高到其中。后面我们在生成纹理的时候会用图像的宽度和高度。 - 第三个参数:指定图片的
通道(Channel)数量
,但是这里我们只需留为0
。 - 第四个参数:告诉
SOIL2
如何来加载图片:我们只关注图片的RGB
值。结果会储存为一个很大的char/byte
数组。
生成纹理
和之前生成的OpenGL对象一样,纹理也是使用ID引用的。
GLuint texture;
glGenTextures(1, &texture);
glGenTextures
函数首先需要输入生成纹理的数量,然后把它们储存在第二个参数的GLuint
数组中(我们的例子中只是一个单独的GLuint),就像其他对象一样,我们需要绑定它,让之后任何的纹理指令都可以配置当前绑定的纹理:
glBindTexture(GL_TEXTURE_2D, texture);
现在纹理已经绑定了,我们可以使用前面载入的图片数据生成一个纹理了。纹理可以通过glTexImage2D
来生成:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glGenerateMipmap(GL_TEXTURE_2D); //生成所有需要的多级渐远纹理
glTexImage2D
:生成纹理。
- 第一个参数:
纹理目标(Target)
。设置为GL_TEXTURE_2D
意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1D
和GL_TEXTURE_3D
的纹理不会受到影响)。 - 第二个参数:为纹理指定
多级渐远纹理
的级别,如果你希望单独手动设置每个多级渐远纹理
的级别的话。这里我们填0
,也就是基本级别。 - 第三个参数:告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有
RGB
值,因此我们也把纹理储存为RGB
值。 - 第四个和第五个参数:设置最终的纹理的
宽度
和高度
。我们之前加载图像的时候储存了它们,所以我们使用对应的变量(SOIL_load_image
加载图片时传递的宽
和高
)。 - 第六个参数:应该总是被设为
0
(历史遗留问题)。 - 第七个和第八个参数:定义了
源图的格式
和数据类型
。我们使用RGB
值加载这个图像,并把它们储存为char(byte)
数组,我们将会传入对应值。 - 第九个参数:图像数据。(
SOIL_load_image
返回的就是图像数据)
当调用glTexImage2D
时,当前绑定的纹理对象就会被附加上纹理图像
。然而,目前只有基本级别(Base-level)
的纹理图像被加载了,如果要使用多级渐远纹理
,我们必须手动设置所有不同的图像(不断递增第二个参数)。或者,直接在生成纹理之后调用glGenerateMipmap
。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理
。
生成了纹理
和相应的多级渐远纹理
后,释放图像的内存
并解绑纹理对象
是一个很好的习惯。
SOIL_free_image_data(image); //释放图像内存
glBindTexture(GL_TEXTURE_2D, 0); //解除绑定,和解除VBO一样
生成纹理过程完整代码:
int textureWidht, textureHeight;
unsigned char* image = SOIL_load_image("container.jpg", &textureWidht, &textureHeight, 0, SOIL_LOAD_RGB);
//生成纹理
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
//环绕方式-WRAP,默认环绕方式-重复纹理图形GL_REPEAT
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); //对应X轴
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); //对应Y轴
//过滤方式。缩小(GL_TEXTURE_MIN_FILTER)和放大(GL_TEXTURE_MAG_FILTER)都采用线性过滤(GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); //缩小
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //放大
//生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidht, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
//多级渐远纹理
glGenerateMipmap(GL_TEXTURE_2D);
//解除绑定
SOIL_free_image_data(image); //释放图像资源
glBindTexture(GL_TEXTURE_2D, 0); //解除纹理绑定
应用纹理
纹理是矩形形状,所以顶点坐标也应该是绘制矩形,可以采用OpenGL笔记2-绘制三角形最后绘制矩形的方法,只不过顶点数据里还行要加上纹理坐标
。
GLfloat vertices[] = {
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};
由于我们添加了一个额外的顶点属性,我们必须告诉OpenGL我们新的顶点格式:
是不是已经想到之前经常提到的两个函数glVertexAttribPointer
和glEnableVertexAttribArray
了。
不记得了,可以去看看OpenGL笔记2-绘制三角形。
glVertexAttribPointer(2, 2, GL_FLOAT,GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
glEnableVertexAttribArray(2);
注意,我们同样需要调整前面两个顶点属性的步长参数为8 * sizeof(GLfloat)
,因为一个顶点包括8个数据(3个顶点,3个颜色,2个纹理坐标)。
接着我们需要调整顶点着色器
使其能够接受顶点坐标
为一个顶点属性
,并把坐标传给片段着色器
:
#version 330 core
layout (location = 0) in vec3 position; //顶点坐标
layout (location = 1) in vec3 color; //颜色
layout (location = 2) in vec2 texCoord; //纹理坐标
out vec3 ourColor; //输出颜色
out vec2 TexCoord; //输出纹理坐标
void main()
{
gl_Position = vec4(position, 1.0f);
ourColor = color;
TexCoord = texCoord;
}
片段着色器应该把输出变量TexCoord
作为输入变量。
片段着色器也应该能访问纹理对象,但是我们怎样能把纹理对象传给片段着色器呢?
GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler)
,它以纹理类型
作为后缀,比如sampler1D
、sampler3D
,或在我们的例子中的sampler2D
。我们可以简单声明一个uniform sampler2D
把一个纹理添加到片段着色器
中,稍后我们会把纹理赋值给这个uniform
。
片段着色器
#version 330 core
in vec3 ourColor; //顶点着色器传进来的颜色参数
in vec2 TexCoord; //顶点着色器传进来的纹理坐标
out vec4 color; //输出的颜色值
uniform sampler2D ourTexture; //程序传进来的纹理采样器
void main()
{
color = texture(ourTexture, TexCoord);
}
我们使用GLSL内建的texture函数来采样纹理的颜色。
texture
:采样纹理颜色。
- 第一个参数:纹理采样器。
- 第二个参数:纹理坐标。
texture
函数会使用之前设置的纹理参数对相应的颜色值
进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色
。
现在只剩下在调用glDrawElements
之前绑定纹理了,它会自动把纹理赋值给片段着色器的采样器:
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
是不是很奇怪,有在着色器里定义uniform
,为啥没在程序中调用glGetUniformLocation
查找,glUniformXX
更新呢,一个纹理的位置值通常称为一个纹理单元(Texture Unit)
。一个纹理的默认纹理单元
是0
,它是默认的激活纹理单元。所以不需要查找,但如果有多个的话,就需要了。详情可看下面的纹理单元
。
所有工作都弄完啦,现在可以展示下效果了
加载纹理完整代码
#include <glew.h>
#include <glfw3.h>
#include <iostream>
#include <SOIL2.h>
//顶点着色器
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"layout (location = 1) in vec3 color;\n"
"layout (location = 2) in vec2 texCoord;\n"
"out vec3 ourColor;\n"
"out vec2 TexCoord;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(position, 1.0);\n"
" ourColor = color;\n"
" TexCoord = texCoord;\n"
"}\n";
//片段着色器
const char* fragmentShaderSource = "#version 330 core\n"
"in vec3 ourColor;\n"
"in vec2 TexCoord;\n"
"out vec4 color;\n"
"uniform sampler2D ourTexture;\n"
"void main()\n"
"{\n"
" color = texture(ourTexture, TexCoord);\n"
"}\n";
int main()
{
glfwInit(); //必须要将glfw初始化
//告诉GLFW使用OpenGL版本
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); //主版本号
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //次版本号
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //使用的是OpenGL核心模式
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); //不允许调整窗口大小
//创建窗口
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
if (window == nullptr) {
std::cout << "Failed to create GLFW Window" << std::endl;
glfwTerminate(); //销毁窗口与数据
return -1;
}
glfwMakeContextCurrent(window); //将OpenGL指向为当前窗口
glewExperimental = GL_TRUE; //用于告知GLEW使用现化OpenGL技术
//glew初始化
if (glewInit() != GLEW_OK) {
std::cout << "Failed to initialize GLEW" << std::endl;
return -1;
}
//视口
int width = 800, height = 600;
glfwGetFramebufferSize(window, &width, &height); //设置OpenGL渲染窗口的尺寸
glViewport(0, 0, width, height); //设置窗口的维度 前两个参数控制窗口左下角的位置, 第三、四个参数控制渲染窗口的宽度和高度
//顶点着色器
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
glCompileShader(vertexShader);
GLint vertexSuccess;
GLchar vertexInfoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &vertexSuccess);
if (!vertexSuccess) {
glGetShaderInfoLog(vertexShader, 512, nullptr, vertexInfoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << vertexInfoLog << std::endl;
}
//片段着色器
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
glCompileShader(fragmentShader);
GLint fragmentSuccess;
GLchar fragmentInfoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &fragmentSuccess);
if (!fragmentSuccess) {
glGetShaderInfoLog(vertexShader, 512, nullptr, fragmentInfoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << fragmentInfoLog << std::endl;
}
//着色器链接程序
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
GLint programSuccess;
GLchar programInfoLog[512];
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &programSuccess);
if (!programSuccess) {
glGetProgramInfoLog(shaderProgram, 512, nullptr, programInfoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << programInfoLog << std::endl;
}
GLfloat vertices[] = {
//-----位置 -----颜色 ---纹理坐标
0.5f, 0.5f, 0.0f, 1.0f, 0.f, 0.f, 1.0f, 1.0f, //右上
0.5f, -0.5f, 0.f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, //右下
-0.5f, -.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, //左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f //右上
};
//顶点索引
GLuint indices[] = {
0, 1, 3,
1, 2, 3
};
//顶点数据
GLuint VBO, EBO, VAO;
glGenBuffers(1, &VBO); //顶点缓冲对象
glGenBuffers(1, &EBO); //索引缓冲数组
glGenVertexArrays(1, &VAO); //顶点数组对象
glBindVertexArray(VAO);
//顶点数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//顶点索引
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//顶点属性
//顶点坐标
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (GLvoid*)0);
glEnableVertexAttribArray(0);
//颜色
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
//纹理坐标
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
glEnableVertexAttribArray(2);
//加载纹理
int textureWidht, textureHeight;
unsigned char* image = SOIL_load_image("container.jpg", &textureWidht, &textureHeight, 0, SOIL_LOAD_RGB);
//生成纹理
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
//环绕方式-WRAP,默认环绕方式-重复纹理图形GL_REPEAT
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); //对应X轴
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); //对应Y轴
//过滤方式。缩小(GL_TEXTURE_MIN_FILTER)和放大(GL_TEXTURE_MAG_FILTER)都采用线性过滤(GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); //缩小
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //放大
//生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidht, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
//多级渐远纹理
glGenerateMipmap(GL_TEXTURE_2D);
//解除绑定
SOIL_free_image_data(image); //释放图像资源
glBindTexture(GL_TEXTURE_2D, 0); //解除纹理绑定
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
while (!glfwWindowShouldClose(window)) {
//检查GLFW是否退出,即窗口是否关闭了,true代表结束了
glfwPollEvents(); //检查有没有事件发生(键盘输入、鼠标移动),如发生调用对应的回调函数 键盘事件:glfwSetKeyCallback(window, key_callback); key_callback即设定的回调函数
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //清空屏幕所用的颜色,即清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。
glClear(GL_COLOR_BUFFER_BIT); //清空屏幕缓冲,这里是颜色缓冲
//渲染指令
glBindTexture(GL_TEXTURE_2D, texture);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
glfwSwapBuffers(window); //交换颜色缓冲,用来绘制,输出显示在屏幕上
}
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
glDeleteVertexArrays(1, &VAO);
glfwTerminate();
return 0;
}
我们还可以把得到的纹理颜色
与顶点颜色
混合,来获得更有趣的效果。我们只需把纹理颜色与顶点颜色在片段着色器中相乘来混合二者的颜色:
片段着色器
color = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0f);
纹理颜色顶点颜色混合效果
纹理单元
你可能会奇怪为什么sampler2D
变量是个uniform
,我们却不用glUniform
给它赋值。使用glUniform1i
,我们可以给纹理采样器
分配一个位置值,这样的话我们能够在一个片段着色器中设置多个纹理。一个纹理的位置值通常称为一个纹理单元(Texture Unit)
。一个纹理的默认纹理单元是0
,它是默认的激活纹理单元
,所以教程前面部分我们没有分配一个位置值。上面讲过。
纹理单元
的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先激活对应的纹理单元。就像glBindTexture一样,我们可以使用glActiveTexture激活纹理单元,传入我们需要使用的纹理单元:
glActiveTexture(GL_TEXTURE0); //在绑定纹理之前先激活纹理单元,有多少纹理及激活多少,只有一个是默认激活的
glBindTexture(GL_TEXTURE_2D, texture);
激活纹理单元之后,接下来的glBindTexture
函数调用会绑定这个纹理到当前激活的纹理单元,纹理单元GL_TEXTURE0
默认总是被激活,所以我们在前面的例子里当我们使用glBindTexture
的时候,无需激活任何纹理单元。
OpenGL至少保证有16
个纹理单元供你使用,也就是说你可以激活从GL_TEXTURE0
到GL_TEXTRUE15
。它们都是按顺序定义的,所以我们也可以通过GL_TEXTURE0 + 8
的方式获得GL_TEXTURE8
,这在当我们需要循环一些纹理单元的时候会很有用。
我们仍然需要编辑片段着色器
来接收另一个采样器,即多个纹理叠加效果。
#version 330 core
...
uniform sampler2D ourTexture1; //采样器1
uniform sampler2D ourTexture2; //采样器2
void main()
{
color = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, TexCoord), 0.2);
}
最终输出颜色现在是两个纹理的结合。
GLSL内建的mix
函数需要接受两个值作为参数,并对它们根据第三个参数进行线性插值。如果第三个值是0.0
,它会返回第一个输入;如果是1.0
,会返回第二个输入值。0.2
会返回80%
的第一个输入颜色和20%
的第二个输入颜色,即返回两个纹理的混合色
。
我们现在需要载入并创建另一个纹理;记得创建另一个纹理对象,载入图片,使用glTexImage2D
生成最终纹理。对于第二个纹理我们使用一张你学习OpenGL时的面部表情图片。
为了使用第二个纹理(以及第一个),我们必须改变一点渲染流程,先绑定两个纹理到对应的纹理单元,然后定义哪个uniform
采样器对应哪个纹理单元(只有一个无需绑定):
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glUniform1i(glGetUniformLocation(ourShader.Program, "ourTexture1"), 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glUniform1i(glGetUniformLocation(ourShader.Program, "ourTexture2"), 1);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
注意,我们使用glUniform1i
设置uniform
采样器的位置值,或者说纹理单元
。通过glUniform1i
的设置,我们保证每个uniform
采样器对应着正确的纹理单元。你应该能得到下面的结果:
多个纹理加载原代码
#include <glew.h>
#include <glfw3.h>
#include <iostream>
#include <SOIL2.h>
//顶点着色器
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"layout (location = 1) in vec3 color;\n"
"layout (location = 2) in vec2 texCoord;\n"
"out vec3 ourColor;\n"
"out vec2 TexCoord;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(position, 1.0);\n"
" ourColor = color;\n"
" TexCoord = texCoord;\n"
"}\n";
//片段着色器
const char* fragmentShaderSource = "#version 330 core\n"
"in vec3 ourColor;\n"
"in vec2 TexCoord;\n"
"out vec4 color;\n"
"uniform sampler2D ourTexture1;\n"
"uniform sampler2D ourTexture2;\n"
"void main()\n"
"{\n"
" color = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, TexCoord), 0.2);\n"
"}\n";
int main()
{
glfwInit(); //必须要将glfw初始化
//告诉GLFW使用OpenGL版本
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); //主版本号
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //次版本号
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //使用的是OpenGL核心模式
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); //不允许调整窗口大小
//创建窗口
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
if (window == nullptr) {
std::cout << "Failed to create GLFW Window" << std::endl;
glfwTerminate(); //销毁窗口与数据
return -1;
}
glfwMakeContextCurrent(window); //将OpenGL指向为当前窗口
glewExperimental = GL_TRUE; //用于告知GLEW使用现化OpenGL技术
//glew初始化
if (glewInit() != GLEW_OK) {
std::cout << "Failed to initialize GLEW" << std::endl;
return -1;
}
//视口
int width = 800, height = 600;
glfwGetFramebufferSize(window, &width, &height); //设置OpenGL渲染窗口的尺寸
glViewport(0, 0, width, height); //设置窗口的维度 前两个参数控制窗口左下角的位置, 第三、四个参数控制渲染窗口的宽度和高度
//顶点着色器
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
glCompileShader(vertexShader);
GLint vertexSuccess;
GLchar vertexInfoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &vertexSuccess);
if (!vertexSuccess) {
glGetShaderInfoLog(vertexShader, 512, nullptr, vertexInfoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << vertexInfoLog << std::endl;
}
//片段着色器
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
glCompileShader(fragmentShader);
GLint fragmentSuccess;
GLchar fragmentInfoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &fragmentSuccess);
if (!fragmentSuccess) {
glGetShaderInfoLog(vertexShader, 512, nullptr, fragmentInfoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << fragmentInfoLog << std::endl;
}
//着色器链接程序
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
GLint programSuccess;
GLchar programInfoLog[512];
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &programSuccess);
if (!programSuccess) {
glGetProgramInfoLog(shaderProgram, 512, nullptr, programInfoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << programInfoLog << std::endl;
}
GLfloat vertices[] = {
//-----位置 -----颜色 ---纹理坐标
0.5f, 0.5f, 0.0f, 1.0f, 0.f, 0.f, 1.0f, 1.0f, //右上
0.5f, -0.5f, 0.f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, //右下
-0.5f, -.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, //左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f //右上
};
//顶点索引
GLuint indices[] = {
0, 1, 3,
1, 2, 3
};
//顶点数据
GLuint VBO, EBO, VAO;
glGenBuffers(1, &VBO); //顶点缓冲对象
glGenBuffers(1, &EBO); //索引缓冲数组
glGenVertexArrays(1, &VAO); //顶点数组对象
glBindVertexArray(VAO);
//顶点数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//顶点索引
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//顶点属性
//顶点坐标
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (GLvoid*)0);
glEnableVertexAttribArray(0);
//颜色
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
//纹理坐标
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
glEnableVertexAttribArray(2);
//纹理1--------------
//加载纹理
int textureWidht, textureHeight;
unsigned char* image = SOIL_load_image("container.jpg", &textureWidht, &textureHeight, 0, SOIL_LOAD_RGB);
//生成纹理
GLuint texture1;
glGenTextures(1, &texture1);
glBindTexture(GL_TEXTURE_2D, texture1);
//环绕方式-WRAP,默认环绕方式-重复纹理图形GL_REPEAT
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); //对应X轴
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); //对应Y轴
//过滤方式。缩小(GL_TEXTURE_MIN_FILTER)和放大(GL_TEXTURE_MAG_FILTER)都采用线性过滤(GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); //缩小
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //放大
//生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidht, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
//多级渐远纹理
glGenerateMipmap(GL_TEXTURE_2D);
//加载纹理2---------------
image = SOIL_load_image("awesomeface.png", &textureWidht, &textureHeight, 0, SOIL_LOAD_RGB);
//生成纹理
GLuint texture2;
glGenTextures(1, &texture2);
glBindTexture(GL_TEXTURE_2D, texture2);
//环绕方式-WRAP,默认环绕方式-重复纹理图形GL_REPEAT
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); //对应X轴
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); //对应Y轴
//过滤方式。缩小(GL_TEXTURE_MIN_FILTER)和放大(GL_TEXTURE_MAG_FILTER)都采用线性过滤(GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); //缩小
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //放大
//生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidht, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
//多级渐远纹理
glGenerateMipmap(GL_TEXTURE_2D);
//解除绑定
SOIL_free_image_data(image); //释放图像资源
glBindTexture(GL_TEXTURE_2D, 0); //解除纹理绑定
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
while (!glfwWindowShouldClose(window)) {
//检查GLFW是否退出,即窗口是否关闭了,true代表结束了
glfwPollEvents(); //检查有没有事件发生(键盘输入、鼠标移动),如发生调用对应的回调函数 键盘事件:glfwSetKeyCallback(window, key_callback); key_callback即设定的回调函数
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //清空屏幕所用的颜色,即清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。
glClear(GL_COLOR_BUFFER_BIT); //清空屏幕缓冲,这里是颜色缓冲
//渲染指令
glUseProgram(shaderProgram);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glUniform1i(glGetUniformLocation(shaderProgram, "ourTexture1"), 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glUniform1i(glGetUniformLocation(shaderProgram, "ourTexture2"), 1);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
glfwSwapBuffers(window); //交换颜色缓冲,用来绘制,输出显示在屏幕上
}
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
glDeleteVertexArrays(1, &VAO);
glfwTerminate();
return 0;
}
你可能注意到纹理上下颠倒了!这是因为OpenGL要求y
轴0.0
坐标是在图片的底部的,但是图片的y
轴0.0
坐标通常在顶部。一些图片加载器比如DevIL
在加载的时候有选项重置y
原点,但是SOIL
没有。SOIL
却有一个叫做SOIL_load_OGL_texture
函数可以使用一个叫做SOIL_FLAG_INVERT_Y
的标记加载并生成纹理,这可以解决我们的问题。不过这个函数用了一些在现代OpenGL中失效的特性,所以现在我们仍需坚持使用SOIL_load_image
,自己做纹理的生成。
所以修复我们的小问题,有两个选择:
- 1.我们可以改变顶点数据的纹理坐标,翻转y值(用1减去y坐标)。
- 2.我们可以编辑顶点着色器来自动翻转y坐标,替换
TexCoord
的值为TexCoord = vec2(texCoord.x, 1.0f - texCoord.y)
;。
顶点着色器
#version 330 core
layout (location = 0) in vec3 position; //顶点坐标
layout (location = 1) in vec3 color; //颜色
layout (location = 2) in vec2 texCoord; //纹理坐标
out vec3 ourColor; //输出颜色
out vec2 TexCoord; //输出纹理坐标
void main()
{
gl_Position = vec4(position, 1.0f);
ourColor = color;
TexCoord = vec2(texCoord.x, 1.0f - texCoord.y); //翻转位置
}
如果你编辑了顶点数据,在顶点着色器中翻转了纵坐标,你会得到下面的结果: