OpenGL入门之纹理Texture

Posted 木大白易

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OpenGL入门之纹理Texture相关的知识,希望对你有一定的参考价值。

本系列文章为Learn OpenGL个人学习总结!
OpenGL入门(一)之认识OpenGL和创建Window
OpenGL入门(二)之渲染管线pipeline,VAO、VBO和EBO
OpenGL入门(三)之着色器Shader
OpenGL入门(四)之纹理Texture
OpenGL入门(五)之Matrix矩阵操作和坐标系统
OpenGL进阶(一)之帧缓冲FrameBuffer
OpenGL进阶(二)之像素缓冲PixelBuffer

纹理

纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节!我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点,来减小开销!

为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样(采集片段颜色)。之后在图形的其它片段上进行片段插值(Fragment Interpolation)。

纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。

一个纹理坐标:

float texCoords[] = 
    0.0f, 0.0f, // 左下角
    1.0f, 0.0f, // 右下角
    0.5f, 1.0f // 上中
;

纹理环绕方式

当纹理坐标超出默认范围时,可以设置环绕方式来展示不同的视觉效果输出!默认为GL_REPEAT,还有GL_MIRRORED_REPEATGL_CLAMP_TO_EDGEGL_CLAMP_TO_BORDER

前边提到过纹理坐标的分量可以通过STPQ访问:

// set the texture wrapping parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);	// set texture wrapping to GL_REPEAT (default wrapping method)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

纹理过滤

纹理像素(Texture Pixel):组成一张图片的无数个像素点
纹理坐标:设置的顶点数组

OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素,然后进行采样提取纹理像素的颜色。那么就会有一个问题,在对应查找像素的时候,用什么方式去拿像素颜色,OpenGL提供了纹理过滤来帮助我们选择,这里有两个重要的选项!

  • GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。
  • GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。

当进行放大(Magnify)和缩小(Minify)操作的时候可以设置纹理过滤的选项:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); //缩小
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //放大

多级渐变纹理

多级渐变纹理主要是用来解决物体远近,纹理大小,分辨率不同的问题!距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个,且节省内存!

注意: 多级渐远纹理主要是使用在纹理被缩小的情况下的:纹理放大不会使用多级渐远纹理。

另外:OpenGL提供glGenerateMipmap()函数,在创建完一个纹理后调用它OpenGL就会为纹理图像创建一系列多级渐远纹理!

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

...//创建纹理
glGenerateMipmap(GL_TEXTURE_2D);

加载和创建纹理

这里使用stb_image.h库来加载图片!
https://github.com/nothings/stb

使用非常简单,在我们的cpp文件中添加:

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

创建纹理:

int width, height, nrChannels;
//这里会拿到图像的宽高和颜色通道个数
unsigned char *data = stbi_load("../dependency/stb/container.jpg", &width, &height, &nrChannels, 0);

//生成纹理
unsigned int texture1;
glGenTextures(1, &texture1);
glBindTexture(GL_TEXTURE_2D, texture1);//绑定

... //设置环绕和过滤

/**
 * @brief 生成纹理
 * 第1个参数:指定纹理target
 * 第2个参数:指定多级渐远纹理的级别  0:基本级别
 * 第3个参数:纹理存储格式。 这里图像只有RGB
 * 第4,5个参数:纹理的宽高
 * 第6个参数:总设置为0(历史问题)
 * 第7,8个参数:源图像的格式和数据类型
 * 第9个参数:图像数据
*/
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);

...
stbi_image_free(data);//释放图像内存

应用纹理

这里我们绘制一个矩形,并把图片纹理贴在矩形上

    //绘制矩形+纹理
    float 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    // 左上
    ;


    const char *vertexShaderSource = "#version 330 core\\n"
    "layout (location = 0) in vec3 aPos;\\n"
    "layout (location = 1) in vec3 aColor;\\n"
    "layout (location = 2) in vec2 aTexCoord;\\n"
    
    "out vec3 vertexColor;\\n"
    "out vec2 texCoord;\\n"
    "void main()\\n"
    "\\n"
    "   gl_Position = vec4(aPos, 1.0);\\n"
    "   vertexColor = aColor;\\n"
    "   texCoord = aTexCoord;\\n"
    "\\0";

    const char *fragmentShaderSource = "#version 330 core\\n"
    "in vec3 vertexColor;\\n" //接收顶点着色器中的输入变量(名称、类型相同) 
    "in vec2 texCoord;\\n" //接收纹理(名称、类型相同)
    "out vec4 color;\\n"
    "uniform sampler2D texture1;\\n" //声明一个纹理采样器
    "void main()\\n"
    "\\n"
    "   color = texture(texture1, texCoord) * vec4(vertexColor, 1.0);\\n" //将输出颜色设置为纹理  第一个参数纹理采样器,第二个参数纹理坐标
    "\\0";

...

GLSL内建的texture()函数,可以得到一个纹理颜色,第一个参数纹理采样器,第二个参数纹理坐标。

这样我们就能绘制出一个正确的纹理图片了!

需要注意的点:
在前边我们了解了uniform这个变量,这里在片段着色器中声明了一个采样器,但是在代码中却没有给它赋值,目前看工作正常,能够绘制出图片!这是因为OpenGL中有默认的纹理单元0,且这个纹理单元默认激活!

纹理单元

当我们需要绘制多个纹理时,就要使用纹理单元,把纹理单元赋值给采样器,就能够和纹理一一对应起来!

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

激活一个纹理单元之后,调用glBindTexture,就会把纹理绑定到当前已激活的纹理单元!

OpenGL至少保证有16个纹理单元供你使用,也就是说你可以激活从GL_TEXTURE0到GL_TEXTRUE15。它们都是按顺序定义的,所以我们也可以通过GL_TEXTURE0 + 8的方式获得GL_TEXTURE8,这在当我们需要循环一些纹理单元的时候会很有用。

修改一下我们的片段着色器:

const char *fragmentShaderSource = "#version 330 core\\n"
    "in vec3 vertexColor;\\n" //接收顶点着色器中的输入变量(名称、类型相同) 
    "in vec2 texCoord;\\n" //接收纹理(名称、类型相同)
    "out vec4 color;\\n"
    "uniform sampler2D texture1;\\n" //声明一个纹理采样器
    "uniform sampler2D texture2;\\n" 
    "void main()\\n"
    "\\n"
    "   color = mix(texture(texture1, texCoord), texture(texture2, texCoord), 0.2) * vec4(vertexColor, 1.0);\\n" //将输出颜色设置为纹理  第一个参数纹理采样器,第二个参数纹理坐标
    "\\0";

...
//有两个纹理时,就需要设置纹理单元
glUseProgram(shaderProgram);
glUniform1i(glGetUniformLocation(shaderProgram, "texture1"), 0);//指定采样器属于哪个纹理单元
glUniform1i(glGetUniformLocation(shaderProgram, "texture2"), 1);

...
//渲染
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
...

GLSL内建的mix函数需要接受两个值作为参数,并对它们根据第三个参数进行线性插值。如果第三个值是0.0,它会返回第一个输入;如果是1.0,会返回第二个输入值。0.2会返回80%的第一个输入颜色和20%的第二个输入颜色,即返回两个纹理的混合色。

另外一个需要注意的问题:纹理上下颠倒了!这是因为OpenGL要求y轴0.0坐标是在图片的底部的,但是图片的y轴0.0坐标通常在顶部。
stb_image.h能够在图像加载时帮助我们翻转y轴,在加载前调用:

stbi_set_flip_vertically_on_load(true);

这时我们将得到两个纹理叠加的效果!

以上是关于OpenGL入门之纹理Texture的主要内容,如果未能解决你的问题,请参考以下文章

手把手教会OpenGL之纹理贴图包含纹理载入纹理过滤边界处理纹理参数设置(入门级别案例,棋盘)

手把手教会OpenGL之纹理贴图包含纹理载入纹理过滤边界处理纹理参数设置(入门级别案例,棋盘)

OpenGL ES之实现“大头小头”和“头部晃动”的效果

openGL之API学习(二零三)GL_TEXTURE_WRAP_S GL_TEXTURE_WRAP_T

optix之纹理使用

openGL之API学习(二一二)固定管线的纹理