LearnOpenGL从入门到入魔(3):绘制纹理

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 变量

 变量及变量类型:

变量类别 变量类型 描述
void 用于无返回值的函数或空的参数列表
标量 float, int, uint, bool 浮点型,整型,布尔型的标量数据类型
浮点型向量 vec2, vec3, vec4 包含2,3,4个元素的浮点型向量
整数型向量 ivec2, ivec3, ivec4 包含2,3,4个元素的整型向量
无符号整数型向量 uvec2, uvec3, uvec4 包含2,3,4个元素的无符号整型向量
布尔型向量 bvec2, bvec3, bvec4 包含2,3,4个元素的布尔型向量
矩阵 mat2, mat3, mat4等 尺寸为2x2,3x3,4x4等的浮点型矩阵
纹理句柄(浮点型采样类型) sampler2D, sampler3D,samplerCube等 表示2D/3D立方体等纹理的句柄
纹理句柄(整型采样类型) isampler2D, isampler3D,isamplerCube等 表示2D/3D立方体等纹理的句柄
纹理句柄(无符号整型采样) usampler2D, usampler3D,usamplerCube等 表示2D/3D立方体等纹理的句柄

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

//------------------------------------------
// 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);  // 采集纹理的颜色
}
复制代码

 注:对于矩阵的变换和向量与矩阵的运算,请参考线性代数OpebGL变换章节。

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 运算符

优先级 运算符类别 运算符 结合方向
1 (高) 成组操作 () 从左向右
2 数组下标,函数调用与构造函数,访问分量或结构体的字段,后置自增和自减 [] () . ++ – 从左向右
3 前置自增和自减,一元正/负数,一元逻辑非 ++ – + - ! 从右向左
4 乘法,除法 * / 从左向右
5 加法,减法 + - 从左向右
6 关系比较操作 < > <= >= 从左向右
7 相等操作 == != 从左向右
8 逻辑与 && 从左向右
9 逻辑异或 ^^ 从左向右
10 逻辑或 || 从右向左
11 三元选择操作(问号表达式) ?: 从右向左
12 赋值与算数赋值 = += -= *= /= 从右向左
13(最低) 操作符序列 , 从左向右

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

1.2.2 流程控制语句

 GLSL提供了if-elseswitch-case-default(选择),forwhile或do..while(循环),discardreturnbreakcountiune(跳转)控制语句,除了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函数是着色器程序的入口。

1.4 限定符(Qualifiers)

1.4.1 存储限定符

变量名 作用
const 定义一个编译时常量,声明时需初始化且值不能更改
in/centroid in 指定被修饰的变量为着色器的输入
out/centroid out 指定被修饰的变量为着色器的输出
uniform 指定被修饰的变量为全局的,允许任何着色器在任何阶段访问、修改
layout 指定顶点着色器输入变量使用的顶点属性索引值,便于CPU上在配置链接顶点属性

 通过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 提供了一种特殊的限定符用来定义某个变量的值是否可以被函数修改。

变量名 作用
none:defalut 默认使用的缺省限定符,等价于in
in 指明参数传递的是值,并且函数不会修改传入的值
out 指明参数的值不会传入函数,但是在函数内部修改其值,函数结束后其值会被修改
intout 指明参数传入的是引用,如果在函数中对参数的值进行了修改,当函数结束后参数的值也会修改

示例代码

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 的实现可以更快速和低功耗地运行着色器,效率的提高来自于精度的舍弃,如果精度选择不合理,着色器运行的结果会很失真。

变量名 作用
highp 满足顶点着色语言的最低要求。对片段着色语言是可选项
mediump 满足片段着色语言的最低要求,其对于范围和精度的要求必须不低于lowp并且不高于highp
lowp 范围和精度可低于mediump,但仍可以表示所有颜色通道的所有颜色值

 示例代码:

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 使用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 内置变量

变量名 限定符&类型 作用
顶点着色器变量
gl_Position out vec4 顶点位置坐标
gl_PointSize out float 点渲染模式,方形点区域渲染像素大小
gl_ClipDistance[] out float 输出的裁剪距离将和图元进行线性插值,插值距离小于0,则图元部分将剪切掉
gl_VertexID in int 该变量为顶点保存一个整数索引
gl_InstanceID in float 该变量指出了当前被渲染的instance,初始值为0,每新增一个渲染实例该变量加1
片段着色器变量
gl_FragCoord in vec4 表示片元坐标,单位像素
gl_FrontFacing in bool 表示当前片段是否为正向面的一部分,true是,false不是
gl_FragDepth out float 用于在着色器内设置片段的深度值
gl_PointCoord in vec2 表示点渲染模式对应点像素坐标
gl_PrimitiveID in int 表示当前处理的图元ID,它是片段着色器的输入
... ... ...

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

1.5.2 内置函数

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

变量名 作用
● 角和三角函数
radians(degress) 将角度转换为弧度
degrees(radians) 将弧度转换为角度
sin(degress) sin计算
cos(degress) cos计算
tan(degress) tan计算
● 指数函数
pow(x, y) 计算x的y次方
exp(x) 将弧度转换为角度
log(x) 计算x的log值
exp2(x) 计算2的x次方
sqrt(x) 计算x的根号
● 通用函数
abs(x) 求x的绝对值
sign(x) 如果x>0,返回1.0;如果x=0,返回0.0;否则,返回-1
floor(x) 求等于最近的更小的整数大于等于x
round(x) 等于最近的更大的整数小于等于x
mod(x, y) 求模
● 几何运算函数
float length (genType x) 求x长度,如向量
float distance (genType p0, genType p1) 求p0、p1之间的距离
float dot (genType x, genType y) 求x、y的点积
vec3 cross (vec3 x, vec3 y) 求x、y的叉乘
genType normalize (genType x) 归一化计算,得到一个方向与x相同的单位向量
● 矩阵相关函数
mat2 transpose(mat2 m) 求矩阵m的转置矩阵
float determinant(mat2 m) 求矩阵m的行列式
mat2 inverse(mat2 m) 求矩阵的逆矩阵
● 向量相关函数
bvec lessThan(vec x, vec y) 判断向量x < y
bvec greaterThan(vec x, vec y) 判断向量x > y
bvec equal(vec x, vec y) 判断两个向量是否相等,即x==y (大小和方向均一致)
● 纹理相关函数
ivec2 textureSize (gsampler2D sampler, int lod) 获取纹理的大小
gvec4 texture (gsampler2D sampler, vec2 P [, float bias] ) 采集纹理数据
gvec4 textureProj (gsampler2D sampler, vec3 P [, float bias] ) 采集纹理投影数据
... ...

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

2. 纹理

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

2.1 纹素

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

2.3 纹理环绕

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

环绕方式 描述
GL_REPEAT 默认环绕方式,即重复纹理图像
GL_MIRRORED_REPEAT 和GL_REPEAT一样,但每次重复图片是镜像放置的
GL_CLAMP_TO_EDGE 纹理坐标被约束在0到1之间,超出部分会重复纹理坐标的边缘,产生一种边缘被拉伸效果
GL_CLAMP_TO_BORDER 超出的坐标为用户指定的边缘颜色

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

在这里插入图片描述

2.4 纹理过滤

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

  • GL_NEAREST

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

在这里插入图片描述

  • GL_LINEAR

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

在这里插入图片描述

 两种滤波效果如下图:

在这里插入图片描述

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

2.5 多级渐远纹理(Mipmap)

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

image

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

过滤方式 描述
GL_NEAREST_MIPMAP_NEAREST 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样
GL_LINEAR_MIPMAP_NEAREST 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样
GL_NEAREST_MIPMAP_LINEAR 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样
GL_LINEAR_MIPMAP_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新的顶点格式如下:

image

相关代码如下:

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);
复制代码

4. 参考文献

1. LearnOpenGL中文文档
2. OpenGL3 Reference Pages
3. GLSLangSpec.3.30.pdf
4. Pixel vs. Texel


Github源码:LearnOpenGL(如果觉得有用,记得给个小star哈~)

猜你喜欢

转载自juejin.im/post/7031050975720931365