Linux OpenGL 实践篇-5 纹理
纹理
在之前的实践中,我们所渲染的物体的表面颜色都是纯色或者根据顶点位置计算出的一个颜色,这种方式在表现物体细节方面是比较吃资源的,因为我们每增加一个细节,我们就需要定义更多的顶点及其属性。所以美术人员和程序员更多的是使用纹理来表现模型的细节。
纹理简单来说就是一个二维图片,OpenGL通过顶点的UV坐标把图片的内容贴到物体的表面,这样我们只需要少量的顶点和一张贴图就可以表现出足够的细节。可以想象一下,有一面墙,每一块转的纹理不同,如果使用增加顶点数据的方式来渲染,需要的数据不可预计,但如果使用贴图,顶点可以减少到4个,同时细节可由贴图来控制,想要精细的表现,则和使用分辨率大的贴图,否则相反。
在OpenGL中,使用纹理的步骤如下:
- 创建纹理;
- 设置纹理的环绕方式和滤波模式;
- 从外部加载图片并把图片数据填充到纹理;
- 使用纹理;
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 纹理的更多相关文章
- Linux OpenGL 实践篇-16 文本绘制
文本绘制 本文主要射击Freetype的入门理解和在OpenGL中实现文字的渲染. freetype freetype的官网,本文大部分内容参考https://www.freetype.org/fre ...
- Linux OpenGL 实践篇-3 绘制三角形
本次实践是绘制两个三角形,重点理解顶点数组对象和OpenGL缓存的使用. 顶点数组对象 顶点数组对象负责管理一组顶点属性,顶点属性包括位置.法线.纹理坐标等. OpenGL缓存 OpenGL缓存实质上 ...
- Linux OpenGL 实践篇-15-图像数据操作
OpenGL图像数据操作 之前的实践中,我们在着色器中的输入输出都是比较固定的.比如在顶点或片元着色器中,顶点属性的输入和帧缓存的颜色值:虽然我们可以通过纹理或者纹理缓存对象(TBO)来读取任意的内存 ...
- Linux OpenGL 实践篇-13-geometryshader
几何着色器 几何着色器是位于图元装配和片元着色器之前的一个着色器阶段,是一个可选阶段.它的输入是一个图元的完整的顶点信息,通常来自于顶点着色器,但如果细分计算着色器启用的话,那输入则是细分计算着色器的 ...
- Linux OpenGL 实践篇-12-procedural-texturing
程序式纹理 简单的来说程序式纹理就是用数学公式描述物体表面的纹路 .而实现这个过程的着色器我们称之为程序纹理着色器,通常在这类着色器中我们能使用的输入信息也就是顶点坐标和纹理坐标. 程序式纹理的优点 ...
- Linux OpenGL 实践篇-11-shadow
OpenGL 阴影 在三维场景中,为了使场景看起来更加的真实,通常需要为其添加阴影,OpenGL可以使用很多种技术实现阴影,其中有一种非常经典的实现是使用一种叫阴影贴图的实现,在本节中我们将使用阴影贴 ...
- Linux OpenGL 实践篇-10-framebuffer
在之前的实践中我们都是在当前的窗口中渲染,即使用的缓存都是由glutCreateWindow时创建的缓存,我们可称之为默认缓存.它是唯一一个可以被图形服务器的显示系统识别的帧缓存,我们在屏幕上看到的只 ...
- Linux OpenGL 实践篇-9 模型
之前一直渲染箱子,显得有点单调.这一次我们绘制一个用艺术家事先用建模工具创建的模型. 本次实践参考:https://learnopengl-cn.github.io/03%20Model%20Load ...
- Linux OpenGL 实践篇-6 光照
经典光照模型 经典光照模型通过单独计算光源成分得到综合光照效果,然后添加到物体表面特定点,这些成分包括:环境光.漫反射光.镜面光. 环境光:是指不是来特定方向的光,在经典光照模型中基本是个常量. 漫反 ...
随机推荐
- javascript里的循环语句
前序:我一直对于for跟for..in存在一种误解,我觉得for都能把事情都做了,为啥还要for...in...这玩意了,有啥用,所以今天就说说JavaScript里的循环语句. 循环 要计算1+2+ ...
- SoDiaoEditor电子病历编辑器更新至V3版本,愿与各位一路同行!
简单闲聊两句-- 记得刚参加工作那会儿,去医院实施,信息科不远处就是手术室,门口每天都挤满了人,他们中大多数都是等待手术结果的患者家属,有的还会把折叠床带来,应该是陪床有段时间了.有时路过,还会听到一 ...
- Java字符编码浅析
Java基本类型占用的字节数:1字节: byte , boolean2字节: short , char4字节: int , float8字节: long , double注:1字节(byte)=8位( ...
- 利用拷贝data目录文件的方式迁移mysql数据库
其实迁移数据库,一般用sql文件就行,把A服务器数据库的表结构和数据等等导出,然后导入到B服务器数据库, 但是这次数据文件过大,大约有40个G,使用命令行导入,效果不是很好,经常在执行过程中报错.卡死 ...
- C语言程序设计(基础)- 第7周作业
为了防止误解,自从本周开始ppt.pta作业.博客作业的命名均与学校教学周一致. 要求一(20经验值) 完成PTA中题目集名为<usth-C语言基础-第七周作业>和<usth-C语言 ...
- 1013团队Beta冲刺day7
项目进展 李明皇 今天解决的进度 部分数据传递和使用逻辑测试 林翔 今天解决的进度 服务器端查看个人发布的action,修改已发布消息状态的action,仍在尝试使用第三方云存储功能保存图片 孙敏铭 ...
- 展示博客(Beta版本)
团队:xjbz 1. 团队成员博客,源码仓库地址. coding:https://git.coding.net/z404395979/xjbz.git 钟平辉(071):http://www.cnbl ...
- 关于FPGA随笔
verilog与c
- Twisted 使用多线程
Twisted 提供主线程和辅线程,主线程只有1个,即reactor.run(),辅线程有多个,可以自由配置 Twisted 大多数代码运行在主线程中,dataReceived(),connectio ...
- DBA 小记 — 分库分表、主从、读写分离
前言 我在上篇博客 "Spring Boot 的实践与思考" 中比对不同规范的 ORM 框架应用场景的时候提到过主从与读写分离,本篇随笔将针对此和分库分表进行更深入地探讨. 1. ...