纹理

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

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

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

  1. 创建纹理;
  2. 设置纹理的环绕方式和滤波模式;
  3. 从外部加载图片并把图片数据填充到纹理;
  4. 使用纹理;

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

  创建纹理

glGenTextures(,&tex);
if(tex == )
{
printf("gen texture fail.\n");
exit();
} 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_S,GL_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,);
if(!data)
{
printf("load texture %s fail.\n",path);
}

  其中path图像文件路径,tex_width,tex_height是图像的宽和高,nChannels表示图像颜色通道个数,返回图像的数据,这个数据存储载在stbi_load动态申请的内存上,所以在使用这个数据填充纹理后,我们需要使用stbi_free释放它。

stbi_image_free(data);

  

  填充纹理

  填充纹理数据的方式根据纹理存储是否可变分为两种,glTexStoage2D,glTexSubImage2D和glTexImage2D。

  第一种:

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

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

  第二种:

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

  使用纹理

  经过上述步骤就可以使用纹理了,使用纹理前需要使用glBindTexture绑定纹理,当然还需要在顶点数据中添加纹理坐标信息。之后我们就可以在着色器中使用了。

  顶点着色器:

#version  core

layout (location=) in vec4 vPosition;
layout (location=) 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 * ;
}

  片元着色器:

#version  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,,);
glFlush();

  效果如图: 

 

  纹理单元

  在上述的纹理使用过程中,我们只使用了一张纹理,因为OpenGL默认的一些行为流程,所以步骤相对简单,但同时也隐藏了一些细节,比如纹理是怎么传给glsl采样器的,采样器到底是什么?我如果想在一个着色器中使用多个纹理怎么弄?在回答这些问题前我们需要弄清两个非常关键的概念:纹理单元和纹理采样器。

  纹理单元在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, , GL_UNSIGNED_INT, );

  着色器代码:

#version  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(,&sp);
glBindSampler(,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找到。

Linux OpenGL 实践篇-5 纹理的更多相关文章

  1. Linux OpenGL 实践篇-16 文本绘制

    文本绘制 本文主要射击Freetype的入门理解和在OpenGL中实现文字的渲染. freetype freetype的官网,本文大部分内容参考https://www.freetype.org/fre ...

  2. Linux OpenGL 实践篇-3 绘制三角形

    本次实践是绘制两个三角形,重点理解顶点数组对象和OpenGL缓存的使用. 顶点数组对象 顶点数组对象负责管理一组顶点属性,顶点属性包括位置.法线.纹理坐标等. OpenGL缓存 OpenGL缓存实质上 ...

  3. Linux OpenGL 实践篇-15-图像数据操作

    OpenGL图像数据操作 之前的实践中,我们在着色器中的输入输出都是比较固定的.比如在顶点或片元着色器中,顶点属性的输入和帧缓存的颜色值:虽然我们可以通过纹理或者纹理缓存对象(TBO)来读取任意的内存 ...

  4. Linux OpenGL 实践篇-13-geometryshader

    几何着色器 几何着色器是位于图元装配和片元着色器之前的一个着色器阶段,是一个可选阶段.它的输入是一个图元的完整的顶点信息,通常来自于顶点着色器,但如果细分计算着色器启用的话,那输入则是细分计算着色器的 ...

  5. Linux OpenGL 实践篇-12-procedural-texturing

    程序式纹理 简单的来说程序式纹理就是用数学公式描述物体表面的纹路 .而实现这个过程的着色器我们称之为程序纹理着色器,通常在这类着色器中我们能使用的输入信息也就是顶点坐标和纹理坐标. 程序式纹理的优点 ...

  6. Linux OpenGL 实践篇-11-shadow

    OpenGL 阴影 在三维场景中,为了使场景看起来更加的真实,通常需要为其添加阴影,OpenGL可以使用很多种技术实现阴影,其中有一种非常经典的实现是使用一种叫阴影贴图的实现,在本节中我们将使用阴影贴 ...

  7. Linux OpenGL 实践篇-10-framebuffer

    在之前的实践中我们都是在当前的窗口中渲染,即使用的缓存都是由glutCreateWindow时创建的缓存,我们可称之为默认缓存.它是唯一一个可以被图形服务器的显示系统识别的帧缓存,我们在屏幕上看到的只 ...

  8. Linux OpenGL 实践篇-9 模型

    之前一直渲染箱子,显得有点单调.这一次我们绘制一个用艺术家事先用建模工具创建的模型. 本次实践参考:https://learnopengl-cn.github.io/03%20Model%20Load ...

  9. Linux OpenGL 实践篇-6 光照

    经典光照模型 经典光照模型通过单独计算光源成分得到综合光照效果,然后添加到物体表面特定点,这些成分包括:环境光.漫反射光.镜面光. 环境光:是指不是来特定方向的光,在经典光照模型中基本是个常量. 漫反 ...

随机推荐

  1. Ubuntu上安装和使用RabbitMQ

    1. 安装RabbitMQ服务软件包 输入以下命令进行安装 #apt install rabbitmq-server 2.安装完成后在rabbitMQ中添加用户 命令:#rabbitmqctl add ...

  2. sharepoint REST API 获取文件夹及文件

    使用REST操作文件夹: 获取文件夹 url: http://site url/_api/web/GetFolderByServerRelativeUrl('/Shared Documents')/f ...

  3. 通过修改然后commit的方式创建自己的镜像

    创建自己的镜像:通过现有的镜像来创建自己的镜像.1.首先拉取一个镜像到本地$ sudo docker imagesREPOSITORY          TAG                 IMA ...

  4. C++标准库vector类型的使用和操作总结

    vector是一种类型对象的集合,它是一种顺序容器,容器中的所有对象必须都是同一种类型.想了解顺序容器的更多内容:C++顺序容器知识总结.vector的对象是可以动态生长的,这说明它在初始化时可以不用 ...

  5. Java多线程:CopyOnWrite容器

    一.什么是CopyOnWrite容器 CopyOnWrite容器即写时复制的容器.通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然 ...

  6. Beta第一天

    听说

  7. 20162308 实验二《Java面向对象程序设计》实验报告

    20162308 实验二<Java面向对象程序设计>实验报告 实验内容 初步掌握单元测试和TDD 理解并掌握面向对象三要素:封装.继承.多态 初步掌握UML建模 熟悉S.O.L.I.D原则 ...

  8. Linux下C编写基本的多线程socket服务器

    不想多说什么,会搜这些东西的都是想看代码的吧. 一开始不熟悉多线程的时候还在想怎么来控制一个线程的结束,后来发现原来有pthread_exit()函数可以直接在线程函数内部调用结束这个线程. 开始还想 ...

  9. C语言——第一次作业

    **学习内容总结** 本周进行了算法的初步学习,用计算机的思维方式去思考问题,并学习了如何用传统程序框图表示算法. 相关内容: 1.算法是用来解决问题的方法与步骤. 2.计算机擅长重复,常用枚举的方法 ...

  10. nyoj 仿射密码

    仿射密码 时间限制:1000 ms | 内存限制:65535 KB 难度:1 描述 仿射密码是替换密码的另一个特例,可以看做是移位密码和乘数密码的结合.其加密变换如下: E(m)=(k1*m+k2) ...