Linux OpenGL 实践篇-5 纹理

纹理

  在之前的实践中,我们所渲染的物体的表面颜色都是纯色或者根据顶点位置计算出的一个颜色,这种方式在表现物体细节方面是比较吃资源的,因为我们每增加一个细节,我们就需要定义更多的顶点及其属性。所以美术人员和程序员更多的是使用纹理来表现模型的细节。

  纹理简单来说就是一个二维图片,OpenGL通过顶点的UV坐标把图片的内容贴到物体的表面,这样我们只需要少量的顶点和一张贴图就可以表现出足够的细节。可以想象一下,有一面墙,每一块转的纹理不同,如果使用增加顶点数据的方式来渲染,需要的数据不可预计,但如果使用贴图,顶点可以减少到4个,同时细节可由贴图来控制,想要精细的表现,则和使用分辨率大的贴图,否则相反。

  在OpenGL中,使用纹理的步骤如下:

  1. 使用glGenTextures创建纹理;

  2. glBindTexture绑定纹理;

  3. glTexParameteri设置纹理的环绕方式和滤波模式;

  4. 从外部加载图片并把图片数据填充到纹理;

  5. 使用纹理,比如使用着色器;

   OpenGL中我们可以使用一维,二维,三维的纹理,下面我们将加载一个二维纹理来作范例:

glGenTextures(1,&tex);
if(tex == 0)
{
        printf("gen texture fail.
");
        exit(1);
}

glBindTexture(GL_TEXTURE_2D,tex);

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_MIRRORED_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);

LoadImage(tex,"./timg.jpeg");

  GL_TEXTURE_WRAP_SGL_TEXTURE_WRAP_T,GL_TEXTURE_WRAP_R表示的是OpenGL中纹理域的S,T,R三个坐标轴,这个三个坐标取值范围控制是(0,1)。当我们传给OpenGL的纹理坐标超过这个范围时,OpenGL需要把这个值再映射到(0,1)的范围内。OpenGL通过设置GL_TEXTURE_WRAP_S,GL_TEXTURE_WRAP_T,GL_TEXTURE_WRAP_R三个采样参数的值控制纹理坐标超出范围后的行为。这个三个参数的取值可以是:GL_CLAMP_TO_EDGE,  GL_CLAMP_TO_BORDER,  GL_REPEAT,  GL_MIRRORED_REPEAT。

  GL_REPEAT           对纹理的默认行为,重复纹理图像

  GL_MIRRORED_REPEAT     和GL_REPEAT一样,不过每次重复图片是镜像放置

  GL_CLAMP_TO_EDGE       纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉神的效果

  GL_CLAMP_TO_BORDER    纹理超出坐标的部分会取用户指定的边缘颜色

  设置好图片的环绕方式后,我们还需要对图片的滤波方式进行设置。纹理映射可以是线性的,平方的或者矩形,甚至三维,但是映射到多边形或者曲面上并变换到屏幕坐标后,纹理的一个纹素很少与屏幕图像的一个像素直接对应。根据使用的变换和纹理映射,屏幕上的一个像素可以对应一个纹素的一部分(放大)或者一个集合的纹素(缩小)。但这中间也有很复杂的情况,比如图像在x方向拉神而在y方向压缩,这个使用图像是放大还是缩小就不是那么好判断了,OpenGL会尽可能给出好的结果(现在有一种“各向异性滤波”的方法可以处理这个问题,但还没有添加到OpenGL核心中,部分OpenGL设备实现在扩展中给出了这一功能)。

  与设置环绕方式一样,我们可以使用glTexParameteri设置GL_TEXTURE_MAG_FILTER,  GL_TEXTURE_MIN_FILTER来控制图形放大缩小时的行为。首先我们说明一下邻近过滤(NEAREST)和线性过滤(LINEAR)。

  NEAREST表示取离纹理坐标最近的纹理像素,如下图所示:

                    

  LINEAR则是取纹理坐标附近的纹理像素线性插值而成,如图:

                    

  邻近取值和线性取值的比较:

            

  GL_TEXTURE_MAG_FILTER只有两个选项:GL_NEAREST, GL_LINEAR。因为在放大的时候,需要的LOD是比最高分辨率更大的mipmap(比默认级别还高),因此放大时只有一个mipmap供选择。

  GL_TEXTURE_MIN_FILTER可选择:除了GL_NEAREST, GL_LINEAR之外,还有GL_NEAREST_MIPMAP_NEAREST,GL_NEAREST,  GL_LINEAR_MIPMAP_NEAREST,  GL_LINEAR_MIPMAP_LINEAR,  GL_NEAREST_MIPMAP_LINEAR,这几个参数的形式可归纳为GL_{A}_MIPMAP_{B},其中A表示mipmap内的纹素混合行为,而B表示mipmap间的纹素混合行为。

   从文件中加载纹理

 

  加载纹理使用stb_image.h。stb_image.h是一个非常流行的加载图片的库,由Sean Barrett开发。它是一个头文件,你可以在你的工程中直接包含它后使用。

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

  其中STB_IMAGE_IMPLEMENTATION宏表示预处理器只包含stb_image.h中只image相关的源码实现。stb_image.h中使用stbi_load来加载图片文件了。

int nChannels;
unsigned char *data = stbi_load(path,&tex_width,&tex_height,&nChannels,0);
if(!data)
{
        printf("load texture %s fail.
",path);
}

  其中参数含义:

  • path,图像文件路径;
  • tex_width,tex_height,是图像的宽和高,
  • nChannels,表示图像颜色通道个数,

  函数返回图像的数据,这个数据存储载在stbi_load动态申请的内存上,所以在使用这个数据填充纹理后,我们需要使用stbi_free释放它。

stbi_image_free(data);  //释放资源

  

  填充数据到OpenGL纹理

  填充纹理数据的方式根据纹理存储是否可变分为两种:

  1. glTexStoage2D,用于可变纹理,配合glTexSubImage2D填充图像数据;
  2. glTexImage2D,用于不可变纹理;

  glTexStoage2D

glTexStorage2D(GL_TEXTURE_2D,2,GL_RGB8,tex_width,tex_height);

/*GLenum err = glGetError();
const GLubyte* content = gluErrorString(err);
printf("error:%s
",content);*/
glTexSubImage2D(GL_TEXTURE_2D,0,
                0,0,
                tex_width,tex_height,
                GL_RGB,GL_UNSIGNED_BYTE,
                data);

  glTexImage2D

glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,tex_width,tex_height,0,GL_RGB,GL_UNSIGNED_BYTE,data);

 绘制纹理

  接着我们使用glBindTexture绑定纹理,就可以在着色器中使用这个纹理了。注意,我们需要在顶点数据中添加纹理坐标信息。

  顶点着色器:

#version 330 core

layout (location=0) in vec4 vPosition;
layout (location=1) in vec2 in_tex_coord;

out vec2 vs_tex_coord;

uniform mat4 ModelViewMatrix;
uniform mat4 ProjectionMatrix;

void main()
{
        gl_Position = ProjectionMatrix * ModelViewMatrix * vPosition;
        vs_tex_coord = in_tex_coord * 2;
}

  片元着色器:

#version 330 core

in vec2 vs_tex_coord;
out vec4 color;

uniform sampler2D tex;

void main()
{
        color = texture(tex,vs_tex_coord);
        //color = vec4(1,0,0,1);
}

  

  绘制代码:

 glClear(GL_COLOR_BUFFER_BIT);
 glBindVertexArray(cubeVAO);
 glBindTexture(GL_TEXTURE_2D,tex);
 glUseProgram(prog);
 glDrawArrays(GL_TRIANGLES,0,6);
 glFlush();

  效果如图: 

 

  多纹理单元

  在上述的纹理使用过程中,我们只使用了一张纹理,因为OpenGL默认的一些行为流程,所以步骤相对简单,但同时也隐藏了一些细节,比如:

  1. 纹理是怎么传给glsl采样器的?
  2. 采样器到底是什么?
  3. 我如果想在一个着色器中使用多个纹理怎么弄?

  在回答这些问题前我们需要弄清两个非常关键的概念:纹理单元和纹理采样器。

  纹理单元在OpenGL中表示的是一个纹理位置。glsl中的纹理采样器(sampler2D等)的需要与一个纹理对应,这样我们才能使用glsl的内置采样函数对这个纹理进行采样。OpenGL中纹理单元的标识格式是GL_TEXTURExx, xx后缀是一个整数,如GL_TEXTURE0表示的就是纹理单元0,也是默认激活的纹理单元。那什么是激活的纹理单元?在OpenGL中,它会保证你至少有15个纹理单元可以使用(GL_TEXTURE0~GL_TEXTURE15),默认情况下除了GL_TEXTURE0外其它的都是未激活,不能使用的。如果我们要使用,应该先激活它,如

glActiveTexture(GL_TEXTURE0); //在绑定纹理之前先激活纹理单元
glBindTexture(GL_TEXTURE_2D, texture);

  OpenGL支持多重纹理,通常OpenGL每个着色器阶段至少支持16个纹理,乘以着色器阶段的数目,OpenGL有80个纹理单元,即GL_TEXTURE0-GL_TEXTURE79。之前我们一直使用一个纹理,即GL_TEXTURE0,这个也是默认的活动纹理单元,所以我们直接使用glBindTexture绑定纹理对象到对应的纹理单元即可,但如果我们想要使用多重纹理,就必须为采样器单元和纹理单元间进行绑定,不然就会出现采样器对同一个纹理单元进行采样的情况。

  如何使用多重纹理?

  •  首先使用glUniform1i建立纹理单元和采样器之间的关联,比如glUniform1i(glGetUniformLocation(program,"texture1"),0)就表示把着色器program中的texture1的采样器和纹理单元0关联起来;
  •  然后绑定纹理到纹理单元,绑定之前需要使用glActiveTexture激活指定的纹理单元,比如激活1号纹理单元:glActiveTexture(GL_TEXTURE1),然后使用glBindTexture绑定纹理即可。

  下面是使用多重纹理的OpenGL代码:

glUniform1i(glGetUniformLocation(program,"texture1"),0);  //绑定纹理单元到glsl采样器
glUniform1i(glGetUniformLocation(program,"texture2"),1);
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);

  着色器代码:

#version 330 core
...

uniform sampler2D texture1;
uniform sampler2D texture2;

void main()
{
    FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}

采样器对象   

  在创建纹理之后,我们队纹理的环绕方式和滤波方式进行了设置,这些设置其实是针对纹理采样器的。一个采样器包括了如何对纹理进行采样的参数设置,比如GL_CLAMP_TO_EDGE。类似与纹理单元,采样器单元表示的也是采样器的位置。我们需要将采样器绑定到对应的采样单元。如果我们没有绑定采样器对象到对应的采样器单元,则可以认为纹理对象包括一个内置的默认值来读取数据的采样器对象。也就是说在上面的例子中当我们使用glBindTexture绑定纹理之后,这个纹理对象包含了一个默认的采样器。

  下面是创建和使用采样器对象的OpenGL代码:

glGenSamplers(1,&sp);
glBindSampler(0,sp);  //0表示采样器单元位置

glSamplerParameteri(GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glSamplerParameteri(GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glSamplerParameteri(GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
glSamplerParameteri(GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGET);

   上述的采样器单元位置就和纹理单元的位置标识一样,如GL_TEXTURE0对应的采样器单元就是0;

参考资料

  本篇实践参考:https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/ 。

  源代码可在https://github.com/xin-lover/opengl-learn找到。

原文地址:https://www.cnblogs.com/xin-lover/p/8673354.html