计算机系统应用教程网站

网站首页 > 技术文章 正文

OpenGL:绘制纹理

btikc 2024-09-02 17:12:28 技术文章 10 ℃ 0 评论


1. GLSL

?GLSL,OpenGL Shader Language缩写,即OpenGL着色器语言,是专门为图形计算器量身定制的类C语言。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;
}

1.1 数据类型

1.1.1 变量

?变量及变量类型:

?GLSL中没有指针类型,对于变量的运算,GLSL要求只有类型一致时,变量才能够完成赋值或其他对应的操作,而对于类型转换可以通过对应的构造器实现。示例代码如下:

【更多音视频学习资料,点击下方链接免费领取,先码住不迷路~】

音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)

//------------------------------------------
// 1. 标量

float a, b=1.5;
int c = 2;
bool success = false;

a = float(c);                   // int类型转float类型

//------------------------------------------
// 2. 向量
// (1)通过向向量构造器传入参数,来创建对应类型的向量;
// (2)使用.或[]操作符访问向量的分量,约定(x,y,z,w)、(r,g,b,a)
//      和(s,t,p,q)分别表示与位置、颜色和纹理坐标相关的向量分量;

vec3 rgb = vec3(1.0);           // 等价于rgb = vec3(1.0, 1.0, 1.0);  
vec3 pos = vec3(1.0, 0.0, 0.5); 
vec4 tmp = vec4(pos, 1.0);      // 提供的参数是向量,该向量的数据类型需一致
                                // 获取位置向量x、y、z轴分量的值
flot x = pos.x;                 // 等价于x = pos[0]
flot y = pos.y;                 // 等价于y = pos[1]
flot z = pos.z;                 // 等价于z = pos[2]

vec3 posNew = pos.xxy;          // 对向量中的元素重新组合,生成一个新的向量

//------------------------------------------
// 3. 矩阵
// (1) 矩阵的值以列的顺序来存储,构造矩阵时参数会按列的顺序填充矩阵;
// (2) 矩阵是向量的组合,可以通过标量、向量或标量和向量的混合来构造,
//     使用[]操作符访问矩阵某一列的值,即该列是一个向量;
// 矩阵案例:  
//      1.0, 0.5, 0,0
//      0.0, 1.0, 1.0
//      0.5, 0.0, 1.0
mat3 tmp1 = mat3(1.0, 0.0, 0.5, // 第一列
                0.5, 1.0, 0.0,  // 第二列
                0.0, 1.0, 1.0); // 第三列
                
mat4 tmp2 = mat4(1.0)           // 标量参数,构造一个4x4的单位矩阵
                                // 向量参数,构造一个3x3的矩阵
vec3 col1 = vec3(1.0, 0.0, 0.5); // 第一列
vec3 col2 = vec3(0.0, 1.0, 0.0); // 第二列
vec3 col3 = vec3(1.0, 1.0, 1.5); // 第三列
mat3 tmp3 = mat3(col1, col2, col3);

// 4. 纹理采样类型
// (1) sampler(采样器)是GLSL提供的可供纹理对象使用的内建数据,其中,
//     sampler1D,sampler2D,sampler3D 表示不同维度的纹理类型;
// (2) sampler通常在片元着色器中内定义,被uniform修饰符修饰,表示这个变量是不会被修改的;
// (3) sampler不能被外部赋值,只能通过GLSL提供的内置函数赋值
//     比如texture2D函数来采样纹理的颜色值(坐标素纹)。

#version 330 core
out vec4 fragColor;                                 // 片段着色器输出
in vec2 textureCoord;                               // 纹理坐标
uniform sampler2D ourTexture;                       // 纹理采样器
void main(){
    fragColor = texture(ourTexture, textureCoord);  // 采集纹理的颜色
}

1.1.2 结构体

?GLSL结构体定义:

struct 结构体名 {
    成员变量;
    成员变量;
    ...
} 结构体类型变量;

GLSL中的结构体定义和使用同C语言,示例代码如下:

struct Light {
 float intensity;
 vec3 position;
} lightVar;

// 等价于
// struct Light {
//   float intensity;
//   vec3 position;
// };
// Light lightVar; // Light为新创建的结构体类型

// 访问结构体成员变量
float intensity = lightVar.lightVar;
vec3 pos = lightVar.position

1.1.3 数组

?GLSL中创建数组与C语言类似,但需要注意以下两点:

  • 除了 uniform 变量之外,数组的索引只允许使用常数整型表达式;
  • GLSL只支持一维数组,当数组作为函数的形参时必须指定其大小;
// 创建一个数组
// 注:Light为1.1.2定义的结构体类型
float frequencies[3];
uniform vec4 lightPosition[4];
const int numLights = 2;
Light lights[numLights];

float a[5];
float b[] = a;  
float b[5] = a; // 等价

float a[5] = float[5](3.4, 4.2, 5.0, 5.2, 1.1);
float a[5] = float[](3.4, 4.2, 5.0, 5.2, 1.1); // 等价

// 数组赋值
float a[5];
a[0] = 1.0;
a[1] = 0.5;
...
a[4] = 0.5;

1.2 语句

1.2.1 运算符

?绝大多数的运算符与 C 语言中一致。与 C 语言不同的是:GLSL 中对于参与运算的数据类型要求比较严格,即运算符两侧的变量必须有相同的数据类型。对于二目运算符(*,/,+,-),操作数必须为浮点型或整型,除此之外,乘法操作可以放在不同的数据类型之间,如浮点型、向量和矩阵等。

1.2.2 流程控制语句

?GLSL提供了if-else、switch-case-default(选择),for、while或do..while(循环),discard、return、break和countiune(跳转)控制语句,除了discard,其他功能与C一样。

  • 判断语句
// if--else判断
if(boolean_expression) {
   /* 如果布尔表达式为真将执行的语句 */
} else {
   /* 如果布尔表达式为假将执行的语句 */
}

// switch---case判断
switch(expression){
    case constant-expression  :
       statement(s);
       break; /* 可选的 */
    case constant-expression  :
       statement(s);
       break; /* 可选的 */
  
    /* 您可以有任意数量的 case 语句 */
    default : /* 可选的 */
       statement(s);
}

循环语句


// while循环
while(condition) {
   statement(s);
}

// do..while循环
do {
  statement(s);
} while(condition);

discard、break、return和continue


// break用于终止循环;
// continue用于结束本次循环;
// return用于终止循环,并终止当前函数执行,同时返回一个值给函数调用者;
// discard仅作用片段着色器中,用于抛弃片段着色器的当前所有操作

#version 330 core
out vec4 fragColor;                                 // 片段着色器输出
in vec2 textureCoord;                               // 纹理坐标
uniform sampler2D ourTexture;                       // 纹理采样器
void main(){
    // 采集纹理的颜色值textureColor
    // 当纹理的颜色值等于vec3(1.0,0.0,0.0)时
    // 抛弃当前片段着色器的所有操作
    vec4 textureColor = texture(ourTexture, textureCoord);  
    if (textureColor.rgb == vec3(1.0,0.0,0.0)) 
        discard; 
    fragColor = textureColor
}

1.3 函数

?GLSL中函数定义:

// 函数声明
returnType functionName (type0 arg0, type1 arg1, ..., typen argn);

// 函数定义
returnType functionName (type0 arg0, type1 arg1, ..., typen argn)
{
 // do some computation
 return returnValue;
}

?由上述定义可知,GLSL中的函数声明、定义和使用都是与C一致的,即先声明再使用,并且如果函数没有返回值,需将返回值类型设定为void,并且函数的参数需要指定具体类型,并指定限定符(in/out/inouts)、const修饰参数(可选)。GLSL中函数名可以被重栽,只要相同函数名的参数列表(主要是指参数的类型和个数)不同就可以了。mian函数是着色器程序的入口。

【更多音视频学习资料,点击下方链接免费领取,先码住不迷路~】

音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)

1.4 限定符(Qualifiers)

1.4.1 存储限定符

通过uniform设置三角形的颜色,示例代码:

// 片段着色器
#version 330 core
out vec4 FragColor;

uniform vec4 ourColor; // 片段着色器输出,在OpenGL程序代码中设定这个变量

void main()
{
    FragColor = ourColor;
}

// 调用glGetUniformLocation函数获取着色器中uniform属性的索引/位置值
// 再调用glUniform4f函数为uniform属性设值
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);

注:Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。

?注:varying、attribute已被弃用。

1.4.2 参数限定符

?GLSL 提供了一种特殊的限定符用来定义某个变量的值是否可以被函数修改。

示例代码:

vec4 myFunc(inout float myFloat, // inout parameter
            out vec4 myVec4, 	 // out parameter
            mat4 myMat4); 		 // in parameter (default)

1.4.3 精度限定符

?OpenGL ES 与 OpenGL 之间的一个区别就是在 GLSL 中引入了精度限定符。精度限定符可使着色器的编写者明确定义着色器变量计算时使用的精度,变量可以选择被声明为低、中或高精度。精度限定符可告知编译器使其在计算时缩小变量潜在的精度变化范围,当使用低精度时,OpenGL ES 的实现可以更快速和低功耗地运行着色器,效率的提高来自于精度的舍弃,如果精度选择不合理,着色器运行的结果会很失真。

?示例代码:

lowp float color;
out mediump vec2 P;
lowp ivec2 foo(lowp mat3);
highp mat4 m;

?除了精度限定符,还可以指定默认使用的精度。如果某个变量没有使用精度限定符指定使用何种精度,则会使用该变量类型的默认精度。默认精度限定符放在着色器代码起始位置,以下是一些用例:

precision highp float;
precision mediump int;

?当为 float 指定默认精度时,所有基于浮点型的变量都会以此作为默认精度,与此类似,为 int 指定默认精度时,所有的基于整型的变量都会以此作为默认精度。在顶点着色器中,如果没有指定默认精度,则 int 和 float 都使用 highp,即顶点着色器中,未使用精度限定符指明精度的变量都默认使用最高精度。在片段着色器中,float 并没有默认的精度设置,即片段着色器中必须为 float 默认精度或者为每一个 float 变量指明精度。

1.4.4 不可变限定符

?当着色器被编译时,编译器会对其进行优化,这种优化操作可能引起指令重排序(instruction reordering),指令重排序可能引起的结果是当两个着色器进行相同的计算时无法保证得到相同的结果。通常情况下,不同着色器之间的这种值的差异是允许存在的。如果要避免这种差异,则可以将变量声明为invariant,可以单独指定某个变量或进行全局设置。

示例代码:

// 使已经存在的变量值不可变
invariant gl_Position; // make existing gl_Position be invariant
out vec3 Color;
invariant Color; // make existing Color be invariant

// 声明一个变量时,使其不可变
invariant centroid out vec3 Color;

1.5 内置变量与内置函数

1.5.1 内置变量

?GLSL中还内置了其他的一些变量,我们在后续的学习中具体详说。

1.5.2 内置函数

?GLSL中内置了很多函数实现对标量和向量的操作。

?GLSL中还内置了很多其他函数,我们在后续的学习中具体详说。

2. 纹理

?纹理(TEXTURE),即物体表面的样子。在计算机的世界中,我们能够绘制的仅仅是一些非常基础的形状,比如点、线、三角形,这些基础显然是无法将一个现实世界中的物体很好的描述在屏幕上的。通常我们通过纹理映射将物体表面图片贴到物体的几何图形上面,完成贴图的过程,将物体从现实世界中模拟到虚拟世界中。简单来说,纹理就是一副只读图像,而把一副图像映射到图形上的过程叫做纹理映射。从另一个角度来说它是一个只读数据容器,而通常它用于存储图像信息。

2.1 纹素

?纹素,即纹理元素(Texel),是纹理图形的基本单元,用于定义三维对象的曲面。3D 对象曲面的基本单位是纹理,而 2D 对象由像素(Pixel)组成。在计算机图形学当中,纹素代表了映射到三维表面的二维纹理的最小单元,纹素和像素很相似因为都代表了图像的基础单元,但是在贴图中的纹素和图片中的像素在显示的时候是有区别的,在特殊的3D映射情况下,纹素和像素可以一一对应,但大多数情况下,他们并不能直接映射。 包含3D纹理的对象靠近观看时纹素看起来相对较大,每个纹素可能存在好几个像素,并且容易看出纹理样式。当相同的纹理对象被移到更远的距离时,纹理映射图案看起来越来越小。最终,每个纹素可以变得小于像素。然后平均采用会被用来组合几个纹素成为一个像素。如果对象足够远,或者其中一个小面与观看视线夹角形成锐角,那么纹素可能变得如此之小,使得图案的本质在观察图像中丢失。

?纹理图像数据存在多种彩色格式(PixelFormat)和多种存储格式(PixelType),比如GL_RGB、GL_RGBA、GL_LUMINANCE和GL_UNSIGNED_SHORT_4_4_4_4、GL_UNSIGNED_SHORT_5_6_5等,这两个属性共同决定了每一个纹理数据单元(纹素)的构成。

2.2 纹理坐标

?众所周知,屏幕的坐标以屏幕左上角为原点,从原点出发沿右方向为x轴,沿下方向为y轴,坐标系中的每一个坐标顶点就是一个像素。同理,纹理也有自己的坐标,它是以纹理(位图)的左下角为原点,从原点出发沿右方向为s轴,沿上方向为t轴,坐标系中的每一个坐标顶点就是一个纹素,每个纹素的坐标范围位于(0,0)~(1,1)之间。屏幕坐标系和纹理坐标系示意图如下:

?在上文中我们了解到,OpenGL的顶点坐标系是在世界坐标系上,当顶点坐标经过顶点着色器处理后,将会被转换(归一化)为标准化设备坐标。假如我们需要将纹理图片映射到OpenGL绘制的正方形上,就需要指定四边形的每个顶点各自对应纹理的哪个部分,这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从哪个部分采样(Sampling)得到对应纹理颜色值,而这个映射的过程被称之为纹理映射或贴图,比如将纹理的ABCD坐标映射到正方形顶点abcd的结果如下图(三)所示:

?或许你会疑惑,为什么图(三)映射的结果图片是倒置的?这是因为纹理的生成是由图片像素来生成的,而图片的存储是从左上角开始的,由此可知,图片左上角像素生成的纹理部分就在纹理左下角处,即图片的左上角对应到了纹理的左下角,上下颠倒了。因此,如果要使图片能够按正常方向贴到正方形上,就需要对纹理坐标进行上下调换,即A与D调换,B与C调换。在OpenGL代码中,顶点坐标和纹理坐标对应关系:

// 贴图倒置
float mVertices[] = {
	// 顶点坐标           // 颜色             // 纹理坐标
	 0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f, // B
	 0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f, // C
	-0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f, // D
	-0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 1.0f  // A
};

// 贴图正常
float mVertices2[] = {
	// 顶点坐标           // 颜色             // 纹理坐标
	 0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 0.0f, // B
	 0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 1.1f, // C
	-0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 1.0f, // D
	-0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 0.0f  // A
};

【更多音视频学习资料,点击下方链接免费领取,先码住不迷路~】

音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)

2.3 纹理环绕

?纹理坐标的范围通常是从[0, 0]到[1, 1],超过[0.0, 1.0]的范围是允许的,而对与超出范围的内容要如何显示,这就取决于纹理的环绕方式(Wrapping mode)。OpenGL提供了多种纹理环绕方式:

各种环绕方式效果如下图所示:

2.4 纹理过滤

?纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值,所以OpenGL需要知道怎样将 纹理像素(Texture Pixel)映射到纹理坐标。当你有一个很大的物体但是纹理的分辨率很低的时候这就变得很重要了,OpenGL对于上述情况会进行纹理过滤(Texture Filtering),主要提供了GL_NEAREST和GL_LINEAR两种过滤方式,这两种滤波方式依据不同的算法会得出不同的像素结果。

  • GL_NEAREST

?Nearest Neighbor Filtering,即邻近滤波,是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:

  • GL_LINEAR

?(Bi)linear Filtering,即线性滤波,它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色。

两种滤波效果如下图:

?由上图可知,GL_NEAREST产生了颗粒状的图案,我们能够清晰看到组成纹理的像素,而GL_LINEAR能够产生更平滑的图案,很难看出单个的纹理像素。相比于GL_NEAREST,GL_LINEAR可以产生更真实的输出。

2.5 多级渐远纹理(Mipmap)

?想象一下,假设我们有一个包含着上千物体的大房间,每个物体上都有纹理。有些物体会很远,但其纹理会拥有与近处物体同样高的分辨率。由于远处的物体可能只产生很少的片段,OpenGL从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物体上这会产生不真实的感觉,更不用说对它们使用高分辨率纹理浪费内存的问题了。OpenGL使用一种叫做多级渐远纹理(Mipmap)的概念来解决这个问题,它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。让我们看一下多级渐远纹理是什么样子的:

?OpenGL提供了glGenerateMipmaps函数为每个纹理图像创建一系列多级渐远纹理。特别的,在渲染中切换多级渐远纹理级别(Level)时,OpenGL在两个不同级别的多级渐远纹理层之间会产生不真实的生硬边界。就像普通的纹理过滤一样,切换多级渐远纹理级别时你也可以在两个不同多级渐远纹理级别之间使用NEAREST和LINEAR过滤。为了指定不同多级渐远纹理级别之间的过滤方式,OpenGL提供了以下过滤方式来替换原有的GL_NEAREST和GL_LINEAR方式。

3. OpenGL实战:绘制纹理

?纹理图像可能被储存为各种各样的格式,每种都有自己的数据结构和排列。OpenGL官方推荐使用stb_image.h来加载这些图像,stb_image.h是SeanBarrett的一个非常流行的单头文件图像加载库,它能够加载大部分流行的文件格式,并且能够很简单得整合到我们的工程之中。 将一张图像(纹理)映射到一个正方形上的步骤如下:

(1)创建纹理对象,并将其绑定到目标类型GL_TEXTURE_2D;

// 函数原型:void glGenTextures(GLsizei n, GLuint * textures);
// 
// 函数作用:创建一个纹理对象,并返回唯一的ID
// 参数说明:
// n 表示要创建的纹理对象数量;
// textures 表示要创建的所有纹理对象对应的ID
GLuint mTextureId = NULL;
glGenTextures(1, &mTextureId);

// 函数原型:void glBindTexture(GLenum target,GLuint texture);
// 
// 函数作用:绑定纹理对象到纹理目标GL_TEXTURE_2D
// 参数说明:
// target 指定纹理要绑定到的目标类型;
// texture 表示纹理对象对应的ID;
glBindTexture(GL_TEXTURE_2D, mTextureId);

注:调用glDeleteTextures()函数释放这些纹理对象资源。

(2)设置纹理环绕和滤波方式;

// 函数原型:void glTexParameteri(GLenum target,GLenum pname,GLint param);
// 
// 函数作用:设置纹理参数,比如环绕方式、滤波方式等
// 参数说明:
// target 表示纹理对象ID;
// pname 表示具体的纹理参数,比如GL_TEXTURE_WRAP_S为S轴的环绕方式;
// param 指定纹理参数的值,详细参考纹理主要的环绕方式和滤波方式;
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // 设置s轴环绕方式为GL_REPEAT
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); // 设置t轴环绕方式为GL_REPEAT
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // 设置缩小过滤方式为线性过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 设置放大过滤方式为线性过滤

(3)读取本地图片,生成纹理;

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

// 读取本地图片到内存
int imageWidth, imageHeight, imageChannelCount;
unsigned char* data = stbi_load("D:/LearnOpenGL/src/lesson03/zhanlang.jpg", &imageWidth, &imageHeight, &imageChannelCount, 0);
if (!data) {
	std::cout << "load image failed" << std::endl;
	return;
}

// 函数原型:void glTexImage2D(GLenum target,GLint level,
// 	                            GLint internalformat,GLsizei width,
// 	                            GLsizei height,GLint border,GLenum format,
//                          	GLenum type,const void * data)
// 
// 函数作用:指定一个二维的纹理位图(Texture Image)
// 参数说明:
// target 指定纹理目标;
// level 指定图片级别,0为基本图像级别;
// internalformat 指定纹理中颜色分量的数量,比如GL_R表示图片像素包含R、G、B分量;
// width 表示图片的宽度;
// height 表示图片的高度;
// border 具体含义不清楚,固定为0;
// format 指定图片的颜色格式,比如GL_RGB;
// type 指定像素数据的数据类型,比如GL_UNSIGNED_BYTE;
// data 指定图片数据;
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, imageWidth, imageHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, data);

// 函数原型:void glGenerateMipmap(GLenum target);
// 
// 函数作用:为指定的纹理对象生成Mipmap(多级渐远纹理)
// 参数说明:
// target 表示纹理对象ID;
glGenerateMipmap(GL_TEXTURE_2D);
	
// 获取纹理后,释放图片资源
stbi_image_free(data);

注:通过定义STB_IMAGE_IMPLEMENTATION,预处理器会修改头文件,让其只包含相关的函数定义源码,等于是将这个头文件变为一个 .cpp 文件了。

(4)应用纹理。

?当加载完纹理后,接下来就是如何将纹理映射到OpenGL绘制的正方形上,这个过程也称“贴图”。我将这部分分为三个步骤:首先,修改我们之前的顶点数据,即告诉OpenGL如何采样纹理。每个顶点包含位置、颜色、纹理坐标属性,且顶点位置属性坐标要与纹理坐标映射一一对应。然后,再链接各个顶点属性。OpenGL新的顶点格式如下:

相关代码如下:

float vertices[] = {
	 // 顶点坐标           // 颜色             // 纹理坐标
	 0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 0.0f, // top right
	 0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 1.1f, // bottom right
	-0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 1.0f, // bottom left
	-0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 0.0f  // top left 
};

// 链接位置属性,layout=0
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 链接颜色属性,layout=1
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// 链接纹理坐标属性,layout=2
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 aTextureCoord;
out vec3 ourColor;                   // 着色器输出
out vec2 ourTextureCoord;
void main()
{
    gl_Position = vec4(aPos, 1.0);
	ourColor = aColor;
	ourTextureCoord = aTextureCoord;
};

// 片段着色器源码
#version 330 core
out vec4 rgbColor;  // 片段着色器输出
in vec3 ourColor;   // 片段着色器输入
in vec2 ourTextureCoord;
uniform sampler2D ourTexture; // 采样器
void main()
{
    // 根据纹理坐标对纹理进行采样
    // 将采样得到的纹理颜色颜色值赋值给rgbColor
   rgbColor = texture(ourTexture, ourTextureCoord);
};

?最后,将纹理对象绑定到纹理目标上,之后调用glDrawElements函数实现纹理颜色渲染,即该函数被调用后会自动把纹理赋值给片段着色器的采样器。代码如下:

glUseProgram(mShaderProgramId);
glBindVertexArray(mVAOId);
// 函数原型:void glBindTexture(GLenum target,GLuint texture)
// 
// 函数作用:将纹理对象绑定到纹理目标上
// 参数说明:
// target 用于指定要绑定到的纹理目标;
// texture 用于指定被绑定的纹理对象ID;
glBindTexture(GL_TEXTURE_2D, mTextureId);

glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

如果你对音视频开发感兴趣,觉得文章对您有帮助,别忘了点赞、收藏哦!或者对本文的一些阐述有自己的看法,有任何问题,欢迎在下方评论区讨论!

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表