LearnOpenGL学习笔记
入门
认识OpenGL
OpenGL并不是一个API,而是规范。OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现的,将由OpenGL库的开发者自行决定。
核心模式和立即渲染模式
早期的OpenGL使用立即渲染模式(固定渲染管线),该模式容易理解和使用,但是效率太低。因此从OpenGL3.2开始,规范开始废弃立即渲染模式。
使用OpenGL核心模式,OpenGL迫使我们使用现代的函数,当试图使用一个已废除的函数,OpenGL会抛出一个异常,并停止绘图。现代函数要求使用者真正理解OpenGL和图形编程,它有一些难度,然而提供了更多的灵活性,更高的效率,更重要的是可以更深入的理解图形编程。
扩展
OpenGL的一大特性是对扩展的支持,当一个显卡公司提出一个新特性或者是渲染上的大优化,通常会以扩展的方式在驱动中实现。
状态机
OpenGL自身也是一个巨大的状态机:一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态也被称为OpenGL上下文(context)。通常使用如下途径去更改OpenGL状态,设置选项,操作缓冲。最后使用当前OpenGL上下文来渲染。
对象
OpenGL库是用C语言写的,同时也支持多种语言的派生,但其内核仍是C库。由于C的一些语言结构,不容易被翻译到其他的高级语言,因此OpenGL开发时引入了一些抽象层,"对象Object"就是其中一个。
在OpenGL中,对象指的是一些选项的集合,它代表OpenGL状态的一个子集。
使用OpenGL的类型的好处是保证了在各平台中每一种类型的大小都是统一的。你也可以使用其它的定宽类型(Fixed-width Type)来实现这一点。
使用对象的好处在于在程序中,不止可以定义一个对象,并设置他们的选项,每个对象都可以是不同的设置,在执行一个使用OpenGL状态的操作的时候,只需要绑定含有需要设置的对象即可。
创建窗口
在画出出色的效果之前,首先要做的是创建一个OpenGL上下文(context)和一个用于显示的窗口。这些操作在不同的系统上是不同的,OpenGL有目的地从这些操作抽象(abstract)出去,我们不得不自己处理创建窗口,定义OpenGL上下文以及处理用户输入。
视口
首先需要设置OpenGL使用的版本,判断是否支持该版本的OpenGL。初始化一些配置。
在渲染之前,需要设置OpenGL渲染窗口的尺寸大小,即视口。这样OpenGL才只能知道怎样根据窗口大小显示数据和坐标。我们可以通过调用glViewport函数来设置窗口的维度。
glViewport(x,y,w,h);
glViewport函数前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度(像素)。
当用户更改窗口的大小的时候,视口也应该被调整。
渲染循环
并不希望只能绘制一下就停止,希望程序在主动关闭之前,都能够不断的绘制图像,并能够接受用户的输入,在程序主动关闭前一直重新渲染当前帧称为渲染循环。
双缓冲(Double Buffer)
应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了
释放资源
当渲染循环结束后我们需要正确释放/删除之前的分配的所有资源。
输入事件
根据所要的效果,监听事件,更改内容,在循环渲染中调用。
渲染
要把所有的渲染(Rendering)操作放到渲染循环中,这样渲染指令在每次渲染循环迭代的时候都能被执行。
在每个新的渲染迭代开始之前,总是希望清屏,否则仍能看到上一次的迭代结果。可以通过调用glClear函数来清空屏幕的颜色缓冲,它接受一个缓冲位(Buffer Bit)来指定要清空的缓冲,可能的缓冲位有GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT。
glClearColor来设置清空屏幕所用的颜色。
glClearColor函数是一个状态设置函数,而glClear函数则是一个状态使用的函数,它使用了当前的状态来获取应该清除为的颜色。
你好,三角形
基本概念
- 顶点数组对象:Vertex Array Object,VAO
- 顶点缓冲对象:Vertex Buffer Object,VBO
- 索引缓冲对象:Element Buffer Object,EBO或Index Buffer Object,IBO
3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。
图形渲染管线接受一组3D坐标,然后让它们变为在屏幕上显示的2D像素。
显卡中有多个处理核心,每个处理核心在GPU上为每一个渲染管线运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序被叫做着色器。
渲染管线在每个阶段的抽象展示。
顶点着色器:
把一个单独的顶点作为输入,顶点着色器的目标是把一种3D坐标转换为另一种3D坐标,允许对顶点属性做一些处理。
图元装配:
将顶点着色器输出的所有顶点作为输入,并所有的点装配成指定图元的形状。如点、线、三角形。
几何着色器:
把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。
光栅化阶段:
把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
片段着色器:
计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
Alpha测试和混合阶段:
这个阶段检测片段的对应的深度(和模板(Stencil))值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。
顶点输入
OpenGL仅当3D坐标在3个轴x、y、z上都为-1到1的范围内才处理它,所有在所谓的标准化设备坐标范围内的坐标才会最终呈现在屏幕上(在这个范围以外的坐标都不会显示)。
一旦顶点坐标在顶点着色器处理过,就应该是标准化设备坐标了。标准化设备通过OpenGL的glviewport转换变成屏幕空间坐标。
顶点着色器会在GPU上创建内存用于储存顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。
顶点缓冲对象VBO来管理这个内存,他会在GPU内存中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。
OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。
glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。
设置显卡如何管理给定的数据,有三种方式。
- GL_STATIC_DRAW :数据不会或几乎不会改变。
- GL_DYNAMIC_DRAW:数据会被改变很多。
- GL_STREAM_DRAW:数据每次绘制时都会改变。
顶点着色器
使用着色器语言(GLSL)编写着色器,然后编译着色器,就可以在程序中使用它了。
#version 330 core #设置版本以及opengl渲染方式
layout (location = 0) in vec3 aPos; #只关心坐标位置
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
在GLSL中一个向量最多有四个分量,x、y、z、w,其中w指的并不是空间上的位置,而是用在透视除法上。
编译着色器
为了使用着色器,必须在运行时动态编译它的源码。
创建着色器
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
需要传递参数,告知是什么类型的着色器。
把着色器源码附加到着色器对象,然后编译它。
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
vertexShaderSource指的是着色器源码
片段着色器
片段着色器计算像素的最后颜色输出。
在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间。
#version 330 core
out vec4 FragColor; #out关键字声明输出变量
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
着色器程序
着色器程序对象是多个着色器合并之后并最终链接完成之后的版本。如果要使用编译完成的着色器必须把他们链接成一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
激活程序对象,在该函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象。
glUseProgram(shaderProgram);
再把着色器对象链接到程序对象以后,需要删除着色器对象。
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
链接顶点属性
顶点着色器允许指定任何以顶点属性为形式的输入,必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。
使用glVertexAttribPointer
告诉OpenGL该如何解析数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
第一个参数为顶点属性的位置值;第二个属性为指定顶点属性的大小值;第三个参数为指定数据的类型
第四个属性为是否希望数据被标准化
第五个属性为步长,它告诉我们在连续的顶点属性组之间的间隔;第六个属性为它表示位置数据在缓冲中起始位置的偏移量(Offset)
使用glEnableVertexAttribArray
,顶点属性位置值作为参数,以启动顶点属性,顶点属性默认是关闭的。
在OpenGL绘制一个物体的流程为
// 0. 复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. 当我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
// 3. 绘制物体
someOpenGLFunctionThatDrawsOurTriangle();
当顶点属性过多时,绑定正确的缓冲对象,为每个物体配置所有顶点属性就会变成一件麻烦事。为了解决这个问题,引入了顶点数组对象。
顶点数组对象
顶点数组对象可以像顶点缓冲对象一样被绑定,任何随后的顶点属性调用都会储存在这个VAO中,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。填充VBO、配置顶点属性指针以及如何把它们都储存到一个VAO里。
顶点数组对象会储存以下几个信息
- glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
- 通过glVertexAttribPointer设置的顶点属性配置。
- 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。
索引缓冲对象
当要绘制一个四边形,需要的顶点数目为四个,但是由于OpenGL只可以渲染点、线、三角形,以顶点的形式的话需要6个顶点,造成了浪费。尤其是面数过多时,所以引入了索引缓冲对象,只需要指定绘制的顺序就可以。
索引缓冲对象(EBO)也是一个缓冲,专门储存索引。
与VBO的使用方式类似,但是缓冲类型为GL_ELEMENT_ARRAY_BUFFER。
使用glDrawElements
来替换glDrawArrays
函数,来指明我们从索引缓冲渲染。
最后的使用代码为
// ..:: 初始化代码 :: ..
// 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);
[...]
// ..:: 绘制代码(渲染循环中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);
着色器
GLSL
着色器使用GLSL语言写成的,为图形计算量身定制,包含一些针对向量和矩阵操作的有用特性。
着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。
一个典型的着色器
#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
**加粗样式**
int main()
{
// 处理输入并进行一些图形操作
...
// 输出处理过的结果到输出变量
out_variable_name = weird_stuff_we_processed;
}
数据类型
GLSL有两种容器类型:向量(Vector)和矩阵(Matrix)
向量类型
类型 | 含义 |
---|---|
vecn | 包含n个float分量的默认向量 |
bvecn | 包含n个bool分量的向量 |
ivecn | 包含n个int分量的向量 |
uvecn | 包含n个unsigned int分量的向量 |
dvecn | 包含n个double分量的向量 |
输入与输出
每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了in和out关键字来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。
顶点着色器应该接收一种特殊形式的输入,否则就会效率低下。顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。我们已经在前面的教程看过这个了,layout (location = 0)。顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。
也可以忽略layout (location = 0)标识符,通过在OpenGL代码中使用glGetAttribLocation查询属性位置值(Location),但是在着色器中设置它们更容易理解并且节省OpenGL的工作量。
片段着色器
需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。
Uniform
Uniform是一种从CPU的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同,uniform是全局的(global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。
如果你声明了一个uniform变量,但是在GLSL没用过,编译器会默默移除这个变量,导致最后编译出的版本并不会包含它,这可能会导致几个非常麻烦的错误。
float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
使用glGetUniformLocation
可以得到着色器的uniform变量。
查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。
顶点和片段着色器储存可以为两个叫做shader.vs和shader.fs的文件。也可以使用自己喜欢的名字命名着色器文件;后缀名不是固定的。
纹理
基本知识
纹理是一个2D的图片(甚至也有1D和3D的纹理),可以用来添加物体的细节。
为了能把纹理映射到模型上,需要指定顶点各自对应在纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样(采集片段颜色)。
纹理坐标在x和y轴上,范围为0到1之间。使用纹理坐标获取纹理颜色叫做采样。纹理坐标为二维坐标。
纹理环绕方式
纹理坐标的范围通常是从(0,0)到(1,1)。OpenGL默认的行为是重复这个纹理图像。
环绕方式 | 描述 |
---|---|
GL_REPEAT | 对纹理的默认行为。重复纹理图像。 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的。 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色。 |
下图为环绕方式的示例图: | |
![]() | |
使用函数glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT); 第一个参数为纹理目标,第二个参数为我们指定设置的选项与应用的纹理轴,最后的参数为环绕方式。 |
纹理过滤
纹理过滤有很多个选项,最重要的有两种GL_NEAREST和GL_LINEAR。
GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:
当进行放大和缩小的操作时,可以设置纹理过滤的选项。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
多级渐远纹理
OpenGL使用了一种多级渐远纹理,简单来说就是一系列的纹理图像,后一个纹理图像时前一个纹理图像的1/2。。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,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(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
一个常见的错误是,将放大过滤的选项设置为多级渐远纹理过滤选项之一。这样没有任何效果,因为多级渐远纹理主要是使用在纹理被缩小的情况下的:纹理放大不会使用多级渐远纹理,为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM错误代码。
加载与创建纹理
需要写一个图像加载器,把图像转换为字节序列。也可以使用一个支持多种流行格式的图像加载库来为我们解决这个问题。
生成纹理
纹理也是通过ID引用的,创建纹理
unsigned int texture;
glGenTextures(1, &texture);
glGenTextures
函数首先需要输入生成纹理的数量,然后把它们储存在第二个参数的unsigned int数组中。
绑定纹理
glBindTexture(GL_TEXTURE_2D, texture);
通过之前的加载与创建纹理得到的字节序列,生成纹理。
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);//这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
生成了纹理和相应的多级渐远纹理后,记得要释放图像的内存。
生成一个纹理的过程应该为
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 为当前绑定的纹理对象设置环绕、过滤方式
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);
// 加载并生成纹理
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);
应用纹理
如何把纹理对象传递给片段着色器?GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler2D以及sampler3D。
片段着色器例子
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
FragColor = texture(ourTexture, TexCoord);
}
调用glDrawElements
之前绑定纹理了,它会自动把纹理赋值给片段着色器的采样器:
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
纹理单元
一个纹理的位置值常被称为纹理单元。一个纹理的默认纹理单元是0,它是默认的纹理激活单元。
纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先激活对应的纹理单元。
使用方法:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
OpenGL至少保证有16个纹理单元供你使用,也就是说你可以激活从GL_TEXTURE0到GL_TEXTRUE15。它们都是按顺序定义的
使用片段着色器接收两个纹理
#version 330 core
...
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}