OpenGL学习(一)
基本概念
因为 OpenGL ES 是 OpenGL 的一个子集,所以下面就主要介绍一些有关 OpenGL 的一些基本概念。
OpenGL 的结构可以从逻辑上划分为下面 3 个部分:
- 图元(Primitives)
- 缓冲区(Buffers)
- 光栅化(Rasterize)
图元(Primitives)
在 OpenGL 的世界里,我们只能画点、线、三角形这三种基本图形,而其它复杂的图形都可以通过三角形来组成。所以这里的图元指的就是这三种基础图形:
- 点:点存在于三维空间,坐标用(x,y,z)表示。
GL_POINTS
- 线:由两个三维空间中的点组成。
GL_TRIANGLES
- 三角形:由三个三维空间的点组成。
GL_LINE_STRIP
缓冲区(Buffers)
OpenGL 中主要有 3 种 Buffer:
- 帧缓冲区(Frame Buffers) 帧缓冲区:这个是存储
OpenGL 最终渲染输出结果的地方
,它是一个包含多个图像的集合,例如颜色图像、深度图像、模板图像等。 - 渲染缓冲区(Render Buffers) 渲染缓冲区:渲染缓冲区就是一个图像,它是 Frame Buffer 的一个子集。
- 缓冲区对象(Buffer Objects) 缓冲区对象就是程序员输入到 OpenGL 的数据:
- 结构类:称为“
数组缓冲区对象
”或“顶点缓冲区对象
”(“Array Buffer Object”或“Vertex Buff er Object”),即用来描述模型的数组,如顶点数组、纹理数组等; - 索引类:称为“
索引缓冲区对象
”(“Index Buffer Object”),是对上述数组的索引。
- 结构类:称为“
光栅化(Rasterize)
在介绍光栅化之前,首先来补充 OpenGL 中的两个非常重要的概念:
- 顶点(Vertex) 就是
图形中顶点
,一系列的顶点就围成了一个图形。 - 片段(Fragment) 是三维空间的点、线、三角形这些基本图元映射到二维平面上的
映射区域
,是OpenGL渲染一个像素所需的所有数据
而光栅化是把点、线、三角形映射到屏幕上的像素点的过程。
着色器程序(Shader)
Shader 用来描述如何绘制(渲染),GLSL 是 OpenGL 的编程语言,全称 OpenGL Shader Language,语法类似于 C 语言。OpenGL 渲染需要两种 Shader:Vertex Shader
和 Fragment Shader
。
- 顶点着色器(Vertex Shader) 对于3D模型网格的每个顶点执行一次,主要是确定该
顶点的最终位置
。 - 片元着色器(Fragment Shader) 对光栅化之后2D图像中的每个像素处理一次。3D物体的表面
最终显示成什么样
将由它决定,例如为模型的可见表面添加纹理,处理光照、阴影的影响等等。
OpenGL 渲染管线
OpenGL 中有两种渲染管线(Graphics Pipeline),一种是固定流水线
,另外一种则是可编程流水线,通过Shader来控制GPU渲染。
渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。
渲染管线的组成部分:
-
顶点数据数组:其中每个顶点(Vertex)是一个3D坐标的集合,每个顶点的数据是用顶点属性(Vertex Attribute)来描述的;
-
顶点着色器:单个顶点作为输入,将3D坐标(标准设备坐标)转另一种3D坐标(屏幕空间坐标),以及一些顶点属性的处理
-
图元装配:所有顶点作为输入,将这些点装配成指定图元的形状
-
几何着色器:图元形式的顶点集合作为输入,通过产生新顶点构造出新的图元来生成其他图形
-
光栅化:几何着色器的输入作为输入,把图元映射为最终屏幕上相应的像素,生成一堆供片段着色器(Fragment Shader)使用的片段(Fragment),并进行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。生成一堆片段,因此能实现颜色在这些片段上的逐渐变化。
-
片段着色器:光栅化得到的片段作为输入,计算出片段中的每个像素的颜色,最典型的就是通过插值来通过所给的2种颜色得到之间的渐变色。
-
测试和混合:着色后的片段作为输入,检测片段的对应的深度(和模板(Stencil))值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,最后渲染出的像素颜色也可能完全不同。
在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器),而几何着色器一般用GPU自带的。
扩展(Extension)
当一个显卡公司提出一个新特性或者渲染上的大优化,通常会以扩展的方式在驱动中实现。
使用扩展的代码大多看上去如下:
if(GL_ARB_extension_name)
{
// 使用硬件支持的全新的现代特性
}
else
{
// 不支持此扩展: 用旧的方式去做
}
状态机(State Machine)
OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。
OpenGL的状态通常被称为OpenGL上下文(Context),我们使用当前OpenGL上下文来渲染,即通过改变上下文中的某些变量来改变OpenGL的状态,从而下一个绘制命令就会发生改变。
因此,当使用OpenGL的时候,将使用状态设置函数(State-changing Function)来改变上下文;以及使用状态使用函数(State-using Function)来根据当前OpenGL的状态执行一些操作。
对象(Object)
对象:是指一些选项的集合,它代表OpenGL状态的一个子集。
比如,我们可以用一个对象来代表绘图窗口的设置,之后我们就可以设置它的大小、支持的颜色位数等等。可以把对象看做一个C风格的结构体:
struct object_name {
float option1;
int option2;
char[] name;
};
如何使用对象
直接通过OpenGL的API创建对象并使用的方式大概如下:
// 1. 创建对象,并用ID来引用之
unsigned int objectId;
glGenObject(1, &objectId); // 第1个参数个数,第2个参数是要绑定的对象数组
// 2. 绑定对象至上下文(这里绑定到GL_WINDOW_TARGET)
glBindObject(GL_WINDOW_TARGET, objectId);
// 3. 设置当前绑定到 GL_WINDOW_TARGET 的对象的一些选项
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);
// 4. 将上下文对象设回默认,即解除绑定
glBindObject(GL_WINDOW_TARGET, 0);
顶点输入
OpenGL是3D图形库,所有输入的坐标都是3D坐标 \((x,y,z)\)。
对于2D图形,所谓的z轴就是z-order,即堆叠顺序,有时也称为深度(Depth)
标准化设备坐标(NDC, Normalized Device Coordinates):坐标值在 \([-1.0,1.0]\) 的坐标。只有这个范围内的坐标才会显示在屏幕上,超出范围的不显示。
// 上述三角形的坐标定义
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f, // x, y, z
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
屏幕空间坐标(Screen-space Coordinates):通过使用由glViewport
函数提供的数据,进行视口变换(Viewport Transform),标准化设备坐标会变换为屏幕空间坐标。
之后就可以将屏幕空间坐标送给渲染管线的第一个阶段:顶点着色器。
顶点着色器
顶点着色器会在GPU的显存上开辟空间来储存顶点数据,还会配置OpenGL如何解释这些显存,并且指定其如何发送给显卡。
顶点缓冲对象
通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这块显存:
// 1. 创建一个VBO对象
unsigned int VBO;
glGenBuffers(1, &VBO);
// 2. 绑定VBO到GL_ARRAY_BUFFER(允许同时绑定多个缓冲,只要是不同的缓冲类型)
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 3. 准备顶点数据数组
...
// 4. 使用VBO来将顶点数据复制到缓冲的显存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData
的最后一个参数,这个会影响OpenGL将数据放在读写速度不同的显存位置中:
GL_STATIC_DRAW
:数据不会或几乎不会改变。GL_DYNAMIC_DRAW
:数据会被改变很多。GL_STREAM_DRAW
:数据每次绘制时都会改变。
绑定顶点属性(即位解释VBO管理的显存数据)
在渲染前需要告诉OpenGL该如何解释顶点数据,即指定输入的顶点数据的哪一部分对应顶点着色器的哪一个顶点属性。
以前面定义的三角形顶点坐标为例:
假设顶点着色器代码如下:
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
顶点数据对应关系希望是:
即:一共3个坐标,每个坐标有3个分量,每个分量占据 sizeof(float) = 4
个字节,且三个坐标之间是紧挨着存储的。
那现在可以通过glVertexAttribPointer
函数将当前的顶点数组和VBO也就是顶点缓冲对象进行关联:
void glVertexAttribPointer(GLuint index, // GLSL中layout(location = 0)指明的location
GLint size, // 每个顶点属性分量个数
GLenum type, // 每个顶点的数据类型
GLboolean normalized, // 是否标准化(映射到0,1或-1,1之间)
GLsizei stride, // 步长,在连续的顶点属性之间的间隔。设置为0表示OpenGL自行计算,前提是属性之间是紧挨着存储的
const GLvoid *pointer); // 第一个属性的起始地址
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性
0
现在会链接到它的顶点数据。
顶点属性默认是禁用的,现在我们已经定义了OpenGL该如何解释顶点数据,现在应该使用glEnableVertexAttribArray
,以顶点属性位置值作为参数,启用顶点属性:
glEnableVertexAttribArray(0); // 参数是对应的属性下标编号
注意:对于顶点属性和分量的含义,可以看这个例子,进一步深化理解:[着色器 - LearnOpenGL CN (learnopengl-cn.github.io)](https://learnopengl-cn.github.io/01 Getting started/05 Shaders/#_5)
顶点数组对象
顶点数组对象(Vertex Array Object, VAO)就是VBO的一个数组。当VBO很多很多时,用VAO会好一些,因为不用每次都绑定顶点属性和绑定顶点索引数组了,使用起来只需要绑定一次顶点属性即可。当然VAO中所有的VBO用的顶点属性位解释肯定要是一样的:
// ..:: 初始化代码 :: ..
// 0. 创建VAO和VBO
unsigned int VBO, VAO;
glGenVertexArray(1, &VAO);
glGenBuffers(1, &VBO);
// 1. 绑定顶点数组对象
glBindVertexArray(VAO);
// 2. 绑定顶点数组对象并复制到一个顶点缓冲中,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 绑定索引数组对象并复制到一个索引缓冲中,供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 设定顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0); //所有要设置到显存缓冲区的内容都设置完了,因此可以解绑VBO了
//其实这里也可以解绑VAO,因为VAO需要的信息也都设置完了,但是没有必要,因为我们这里只有1个VAO
[...]
// ..:: 绘制代码(渲染循环中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO); // 每次绘图前都要先绑定一下。因为可能存在多个VAO,得让OpenGL知道我们现在用的是哪个VAO
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0); //解绑VAO
VBO和VAO在绑定上的不同
对于VBO:glBindBuffer
用于绑定缓冲区对象,而缓冲区对象可以分为多种类型,例如顶点缓冲区、索引缓冲区等。为了明确指定绑定的是哪种类型的缓冲区对象,glBindBuffer
的设计需要使用 target
参数来指定目标,例如 GL_ARRAY_BUFFER
、GL_ELEMENT_ARRAY_BUFFER
等。
对于VAO:相比之下,glBindVertexArray
的作用对象是顶点数组对象(VAO),而VAO本身就是用于封装多个与顶点属性相关的状态,包括顶点缓冲区绑定、顶点属性指针等。因此,在设计时,OpenGL 将绑定到 GL_VERTEX_ARRAY
目标上的操作都视为与VAO相关的操作。这样,glBindVertexArray
就能够在绑定时自动关联所有与顶点属性有关的状态,而不需要额外的 target
参数。
编写顶点着色器
GLSL语言
顶点着色
// 0. 准备Shader代码(这里是硬编码的,实际要读取文件中的)
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
// 1. 创建着色器对象
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER); // 指定类型是顶点着色器
// 2. 把着色器源码附加到着色器对象上
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);//要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量
// 3. 编译着色器
glCompileShader(vertexShader);
检测编译时错误可以通过以下代码来实现:
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
首先我们定义一个整型变量来表示是否成功编译,还定义了一个储存错误消息的容器。然后我们用glGetShaderiv
检查是否编译成功。如果编译失败,我们会用glGetShaderInfoLog
获取错误消息,然后打印它。
if(!success)
{
glGetShaderInfoLog(vertexShader, sizeof(infoLog), NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
片段着色器
片段着色器所做的是计算像素最后的颜色输出。
在计算机图形中颜色被表示为4维向量:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间。
做法与顶点着色器类似:
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); // 指明类型是片段着色器
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
链接到着色器程序
着色器程序对象(Shader Program Object):多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器,我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序,激活之后,每个着色器调用和渲染调用都会使用链接到这个程序对象上的着色器了。
// 1. 创建着色器程序对象
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
// 2. 把之前编译的着色器附加到着色器程序对象上,然后用glLinkProgram链接它们
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// 3. 激活着色器程序对象
glUseProgram(shaderProgram);
// ...
// 4. 回收资源【一旦完成了着色器的创建、着色器的编译和链接,就可以删除着色器对象了,因为着色器已经写入到GPU中】
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
可以检测链接着色器程序是否失败,并获取相应的日志:
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
...
}
绘制图元
glDrawArrays
函数:使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据来绘制图元。
glDrawArrays(GL_TRIANGLES, 0, 3);
- 第一个参数指定要绘制的图元类型
- 第二个参数指定顶点数组的起始索引,这里是0
- 第三个参数指定要绘制多少个顶点,这里是3
元素缓冲对象
元素缓冲对象(Element Buffer Object,EBO),也叫索引缓冲对象(Index Buffer Object,IBO):一个缓冲区,用于告诉 OpenGL 要绘制VBO中哪些顶点。
比如要绘制一个矩形,但是 OpenGL 只能绘制点、线、三角形,因此给出的顶点数据数组应该是两个三角形拼起来:
float vertices[] = {
// 第一个三角形
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, 0.5f, 0.0f, // 左上角
// 第二个三角形
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
可见 右下角和左上角 的顶点重复了。这样的重复是可以避免的,方法就是向 OpenGL 指定应该绘制所给的顶点数据中哪些顶点。
指定的方式就是给出 EBO,从而实现索引绘制(Indexed Drawing):
// 1. 给出要绘制的顶点数据(不重复)
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角0
0.5f, -0.5f, 0.0f, // 右下角1
-0.5f, -0.5f, 0.0f, // 左下角2
-0.5f, 0.5f, 0.0f // 左上角3
};
// 2. 给出对应这个顶点数据数组,OpenGL应该画的顺序索引:
unsigned int indices[] = {
// 注意索引从0开始!
// 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
// 这样可以由下标代表顶点组合成矩形
// 可以看出我们只用了4个顶点,而不是之前的6个
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
// 3. 创建EBO对象,并将其复制到显存缓冲里
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 绑定缓冲的类型是GL_ELEMENT_ARRAY_BUFFER
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 填写到显存中
// 4. 在绘制图元时,使用glDrawElements而不是glDrawArrays
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glDrawElements
函数:
- 第一个参数指定要绘制的图元类型
- 第二个参数指定要绘制多少个顶点,这里是6
- 第三个参数指定索引的数据类型
- 第四个参数指定顶点数组的起始索引,这里是0
纹理
纹理(Texture):就是一张图片(2D或3D),紧密贴在模型上。
映射(Map):就是将纹理对应到模型上,需要指出每个顶点对应纹理的哪个位置。
纹理坐标(Texture Coordinate):每个顶点都关联一个纹理坐标,表示这个顶点应该从纹理图像的哪个位置采样。没有指定纹理坐标的片段就会进行插值。纹理坐标和之前的标准化设备坐标和屏幕空间坐标都不同,纹理坐标左下角是 \((0,0)\), 右上角是 \((1,1)\)
采样(Sampling):使用纹理坐标获取纹理颜色。
纹理环绕方式(Wrapping)
所谓纹理环绕是对超出纹理坐标范围 \([0,1]\) 的区域进行的绘制方案。
注意,在纹理坐标范围内而又没有指定纹理坐标的是采用的片段插值
环绕方式 | 描述 |
---|---|
GL_REPEAT | 重复纹理图像。【默认】 |
GL_MIRRORED_REPEAT | 重复纹理图像,但每次重复图片是镜像放置的。 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色。 |
这些环绕方式是可以单独对每个坐标轴设置的:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_MIRRORED_REPEAT); // GL_TEXTURE_WRAP_R仅对于3D纹理
如果我们选择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);
纹理过滤(Filtering)
由于纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值,所以很多时候纹理图像和实际屏幕的分辨率是不一样的,所以OpenGL需要知道怎样将纹理像素(Texture Pixel,也叫Texel)映射到纹理坐标。
纹理坐标是你给模型顶点设置的那个数组,OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素,然后进行采样提取纹理像素的颜色。
而纹理像素是组成纹理这个图像的那些像素。
过滤方式 | 描述 | 效果 |
---|---|---|
GL_NEAREST 邻近过滤 | 选择纹理像素中心点最接近纹理坐标的那个像素【默认】 | |
GL_LINEAR 线性过滤 | 基于纹理坐标附近的纹理像素,计算出一个插值 |
邻近插值会保留了图像的颗粒化,线性插值让图像更平滑(模糊)
当纹理进行放大(Magnify)和缩小(Minify)操作的时候可以设置这两种纹理过滤的选项:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // 缩小时采用邻近过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 放大时采用线性过滤
多级渐远纹理(Mipmap)
这个功能是用于一个3D场景,存在着近处的模型和远处的模型,但是这些距离不一的模型都拥有高分辨率纹理。那么对于远处的模型只需要采样几个点就够了,但是纹理分辨率又太高,导致OpenGL在太多像素中采样几个点要经过很多运算,浪费性能。
多级渐远纹理(Mipmap):是一系列的纹理图像,后一个纹理图像是前一个的二分之一。这样按照距离远近来选择最合适大小的纹理(纹理级别Level)。
显然多级渐远纹理应该是自动生成,方法是在创建完一个纹理后调用 glGenerateMipmaps
函数。
在渲染中切换多级渐远纹理级别(Level)时,OpenGL在两个不同级别的多级渐远纹理层之间会产生不真实的生硬边界,所以有不同多级渐远纹理级别之间的过滤方式:
过滤方式 | 描述 |
---|---|
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); // 放大时
加载与创建纹理
和之前生成的OpenGL对象一样,纹理也是使用ID引用的。让我们来创建一个:
// 1. 创建纹理对象
unsigned int texture;
glGenTextures(1, &texture);
// 2. 绑定纹理对象到GL_TEXTURE_2D
glBindTexture(GL_TEXTURE_2D, texture);
// 3. 使用加载的图片数据来生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexImage2D
函数:
- 第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响)。
- 第二个参数为纹理指定多级渐远纹理的级别,这里我们填0,也就是基本级别。
- 第三个参数是把纹理储存为何种格式。我们的图像只有
RGB
值,因此我们也把纹理储存为RGB
值。 - 第四个和第五个参数设置最终的纹理的宽度和高度。
- 第六个参数应该总是被设为
0
(历史遗留的问题)。 - 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为
char
(byte)数组。 - 第九个参数是真正的图像数据。
当调用glTexImage2D时,当前绑定的纹理对象就会被附加上纹理图像。然而,目前只有基本级别(Base-level)的纹理图像被加载了,如果要使用多级渐远纹理,我们必须手动设置所有不同的图像(不断递增第二个参数)。或者,直接在生成纹理之后调用glGenerateMipmap。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
应用纹理
纹理坐标也属于顶点数据,因此要更新顶点数据数组:
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 // 左上
};
从这里也可以看出标准化设备坐标和纹理坐标的不同。
更新顶点数据的位解释:
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
还需要把顶点坐标传给顶点着色器:因为顶点数据得由顶点着色器管(确定纹理图像的位置)
#version 330 core
layout (location = 0) in vec3 aPos; //之前传入的顶点位置数据
layout (location = 1) in vec3 aColor;//之前传入的片段颜色数据
layout (location = 2) in vec2 aTexCoord;//现在传入的纹理坐标数据
out vec3 ourColor;
out vec2 TexCoord;//传给片段着色器的纹理坐标
void main() {
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
TexCoord = aTexCoord;
}
还需要把纹理图像传给片段着色器:因为片段数据得由片段着色器管(进行片段处理和插值)
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;//采样器能接收由CPU程序直接传给片段着色器的纹理数据
void main() {
//使用texture函数来采样纹理的颜色。
//第一个参数是纹理采样器,第二个参数是对应的纹理坐标
FragColor = texture(ourTexture, TexCoord);
}
最后在绘制图元时将纹理数据提供给uniform的采样器:
glUseProgram(shaderProgram);//记得先启用渲染管线上的着色器
glBindTexture(GL_TEXTURE_2D, texture);//绑定一下要用的纹理,确保当期用的纹理是我们要的
glBindVertexArray(VAO);//这里绑定VAO到GL_ARRAY_BUFFER也是同理
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);//使用VAO中的纹理数据绘制一个三角形
总的步骤
// 1. 创建纹理对象
unsigned int texture;
glGenTextures(1, &texture);
// 2. 绑定纹理对象到GL_TEXTURE_2D
glBindTexture(GL_TEXTURE_2D, texture);
// 3. 为当前绑定的纹理对象设置环绕、过滤方式
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);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 4. 使用加载的图片数据来生成纹理(这里用的是std_image库来读取图片)
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D); // 自动生成多级渐远纹理
}
else {
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
标签:OpenGL,TEXTURE,纹理,学习,顶点,GL,着色器
From: https://www.cnblogs.com/3to4/p/17996539