OpenGL(5)之纹理初探

title: OpenGL(5)之纹理初探
date: 2020-07-01 21:33
category: 图形学
tags: opengl
链接:OpenGL(5)之纹理初探

1.概述

首先提出一个问题:什么是纹理?

是由大块的图像数据组成的,可以用来绘制到物体的表面以增强其真实感。(红宝书第六章本章目标中的内容)

再提一个问题:它能做什么?

如上个问题所描述,既然是由大块的图像数据,那么举个例子,在片元处理阶段,可以大量的使用到纹理,即对各个顶点着色的阶段,就将原始的rgba颜色值转换使用纹理相关数据进行上色,这样一来节省了大量ragb颜色值为了满足复杂色值所带来的开销与操作难度。

纹理其实是一个2D图片(也可以是1D或3D的),是由**纹素(texel)**组成,其中通常包含颜色数据信息。

纹理映射的概念
纹理好比一张绘制有砖块的墙纸,然后粘在3D的房子上,这样房子看起来就好像有了砖墙的外表,这就是纹理映射的概念。

将纹理映射到三角形上,需要指定三角形的每个顶点对应的纹理的哪个部分,每个顶点就会关联一个纹理座标,该纹理座标用来表明从纹理图像的哪个部分采样(采集片段颜色),
之后片段着色器通过在这些顶点座标上进行插值即可。

纹理座标在x和y轴上,范围为0到1之间,使用纹理座标获取纹理颜色叫做采样(sampling)。

float texCoords[] ={
	0.0f,0,0f,
	1.0f,0,0f,
	0.5f,1.0f
}

2.纹理环绕方式简介

纹理座标的范围是从(0,0)到(1,1)如果把纹理座标设置在范围之外会发生什么?
OpenGL默认的行为是重复这个纹理图像(比如座标点(0,2)
(1,2)这个范围点是通过重复纹理图像的形式来设置纹理的。)

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

当纹理座标超出默认范围时,视觉效果如下:

关于设置上述环绕方式,可以使用glTexParameter*函数来对单独的一个座标轴进行设置(比如说纹理的座标轴为s和t,(x,y)即你可以对其中的s或者t轴进行设置,设置为针对于S轴进行上述环绕方式进行环绕);

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_MIRRORED_REPEAT);

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,
	GL_MIRRORED_REPEAT);

  1. 第一个参数指定了纹理座标,使用的是2D纹理

  2. 指定设置的选项与应用的纹理轴,人话就是以S或者T轴为轴心进行环绕

  3. 设置环绕方式

如果使用了GL_CLAMP_TO_BORDER选项,即用户为超出范围指定的边缘的颜色,因此这里选择一个glTexParameter函数的fv后缀指定一个颜色。

float[] borderColor[] = {1.0f,1.0f,1.0f,0.0f};
glTexParameterfv(GL_TEXTURE_2D,GL_CLAMP_TO_BORDER,borderColor);

3.纹理过滤

纹理座标不依赖于分辨率,可以是任意浮点值,OpenGL需要知道怎么样将纹理像素映射到纹理座标上;

纹理座标是给模型顶点设置的数组,即与模型的顶点数据进行绑定;

纹理像素图片中一个个的像素点;

OpenGL会通过模型顶点的纹理座标去查找纹理像素,然后通过采样提取像素的颜色,那么就完成了纹理像素通过纹理座标向纹理座标的映射。

当有一个很大的物体但是纹理分辨率很低的时候,我们就需要进行纹理的过滤操作,对于较远的纹素,采用离中心点最接近纹理座标最近的那个像素进行处理,这就是一种纹理的过滤手段。
下面介绍的是过滤方式:

  1. GL_NEAREST(邻近过滤),是OpenGL的一种默认纹理过滤方式,当设置为这种方式的时候,OpenGL会选择中心点最接近纹理座标的那个像素,
  2. GL_LINEAR(线性过滤),会基于纹理座标附近的纹理像素,计算出一个插值,近似这些纹理像素之间的颜色,这样做的好处就是使得图像看起来更加的平滑,视觉效果不会显得突兀,比较顺畅。

下图中的圆圈代表纹理座标点,右边方块为返回的颜色值。

何种情景会使用到这种纹理过滤方式呢?
当需要进行放大和缩小操作时。纹理被缩小的时候可以使用邻近过滤,
当放大的时候使用线性过滤,因为此时如果强行放大,不作处理导致图片被拉伸,图片显得模糊,通过一种线性过滤的方式使得图像更加平滑的过度。

使用glTexParameter*函数为放大和缩小指定过滤方式。

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

多级渐远纹理

纹理拥有与近处物体同样高的分辨率,由于远处的物体可能只产生较少的片段,因此OpenGL要从高分辨率纹理中为较远片段获取正确的颜色值就很困难,因为较远处的颜色信息较少,且需要跨越纹理很大部分的片段采样一个纹理颜色,在小物体上会产生不真实的感觉,再者说对于远处片段使用高分辨率纹理浪费内存。

为了解决上述远处物体的采样问题,OpenGL使用了一种多级渐远纹理概念来解决这个问题,
具体如下:
就是通过一系列的纹理图像,后一个纹理图像是前一个的二分之一;
距离观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,也即是离的多远该用多少分之一的纹理图像,越远纹理图像更小(因为是一直除以2嘛),可以看下图:

其实上述的图是手工画的来模拟创建一系列的多级渐远纹理,OpenGL提供了一个glGenerateMipmaps函数,在创建完一个纹理后,通过调用这个函数,OpenGL会帮助我们完成这些多级渐远纹理的处理。

在渲染中切换多级渐远纹理级别时候,会在不同级别纹理层产生不真实
的生硬边界,因此通过不同多级渐远纹理级别之间的过滤方式替代原有的过滤方式3.纹理过滤 介绍

GL_XXX01_MIPMAP_XXX02;

使用XXX01方式插值进行采样,
使用XXX02方式进行纹理处理;

if (XXX02.container(XXX_NEAREST))
{
	cout<<"使用最邻近多级渐远纹理级别"<<endl;
}else{
	cout<<"两个邻近多级渐远纹理之间采取线性插值"<<endl;
}

过滤方式 描述|
GL_NEAREST_MIPMAP_NEAREST 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样
GL_LINEAR_MIPMAP_NEAREST 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样
GL_NEAREST_MIPMAP_LINEAR 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样
GL_LINEAR_MIPMAP_LINEAR 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR);

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

常见错误:将放大过滤的选项设置为多级渐远纹理过滤选项之一,这是一种错误的理念,因为多级渐远纹理主要是使用在纹理被缩小的情况下

4.加载与创建纹理

在使用纹理之前,需要将纹理加载到应用中,纹理图像可能被存储为各种各样的格式,每种都有自己的数据结构和排列,所以如何才能把这些图像加载到应用中?
通用解决方案:
选一个需要的文件格式 比如.PNG,然后写一个图像加载器,把图像转化为字节序列;

但是文件格式太多,不能都去写支持的加载器。

更好的选择:
使用一个支持多种流行格式的图像加载库来解决这个问题。
比如std_image.h库

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

///......

通过定义STB_IMAGE_IMPLEMENTATION,预处理器会修改头文件,
让其只包含相关的函数定义源码,等于是将stb_image.h头文件变成了一个.cpp文件,只需要在你的程序中包含stb_image.h并且编译就可以了。

使用stb_image.h加载图片,需要使用stbi_load函数:

int width , height,nrChannels;

unsigned char* data = stbi_load("xxx.jpg",&width,&height,&nrChannels,0); // 将图片转换为字符序列

5.生成纹理

  1. 纹理也是使用ID引用的,创建一个纹理对象;
unsigned int texture;
glGenTextures(1,&texture);
  1. 绑定纹理,是为了之后的纹理指令配置得以生效
glBindTexture(GL_TEXTURE_2D,texture);
  1. 纹理绑定之后,通过载入的图片数据生成一个纹理
glTexImage2D(GL_TEXTURE_2D,//1
	0,//2
	GL_RGB,//3
	width,//4
	height,//5
	0,//6
	GL_RGB,//7
	GL_UNSIGNED_BYTE,//8
	data//9
	);
glGenerateMipmap(GL_TEXTURE_2D);

  1. 第一个参数指定了纹理目标,设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象是同一个目标上的纹理。(上述加黑步骤仔细体会)

  2. 第二个参数为纹理指定多级渐远纹理的级别,0为基本级别

  3. 第三个参数告诉OpenGL希望把纹理存储为哪种格式,图像只有RGB值,因此纹理存储为RGB值

  4. 第四个和第五个参数设置最终的纹理的宽度和高度,之前加载图像的时候存储了宽高,所以使用对应的变量。

  5. 第六个参数为历史遗留问题 总是为0

  6. 第七个和第八个定义了源图的格式和数据类型,使用RGB值加载这个图像,并把它们存储为char(byte)数组;

  7. 最后一个参数为真正的图像数据。

上述调用之后,当前绑定的纹理对象就会被附加上纹理图像,
如果要使用多级渐远纹理,可以调用glGenerateMipmap,就会为当前的纹理自动生成所有需要的多级渐远纹理。

纹理生成流程:

unsigned int textureId;
glGenTextures(1,&textureId);
glBindTexture(GL_TEXTURE_2D,texture);

//configrure texture filter and rotate
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
glTExParamteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

// load and generate texture
int width ,height,channels;

unsigned char * data = stbi_load("cc.jpg",&width,&height,&channels,0);

if(data){
	glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,
		width,height,0,GL_RGB,GL_UNSIGNED_BYTE,data);
	glGenerateMipmap(GL_TEXTURE_2D);
}else{
	std::cout<<"Failed to load texture"<<std::endl;
}
stbi_image_free(data);

6.应用纹理

由于需要增加一组纹理座标导致顶点数组的数据越来越多了,这里采用
EBO以及glDrawElements的方式来绘制,这样一来就可以减少顶点数据,节省内存消耗。为了OpenGL能够采样纹理,因此需要将纹理座标添加到顶点数据上,也就是将纹理座标与顶点数据进行一层映射;

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, 0.0f,1.0f,//左上
	0.5f,-0.5f,0.0f,0.0f,0.0f,1.0f,1.0f,0.0f,//右下
	-0.5f,-0.5f,0.0f,1.0f,1.0f,0.0f,0.0f,0.0f//左下

};


添加了一个额外的顶点属性,因此需要更新新的顶点格式,还有要同步更新属性的步长信息,如下图所示

glVertexAttribPointer(2,2,GL_FLOAT,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 aTexCoord;

out vec3 shareColor;
out vec2 texCoord;

void main(){
	gl_Position = vec4(aPos,1.0);
	shareColor = aColor;
	texCoord = aTexCoord;
}

相应的片段着色器如下:

#version 330 core
out vec4 FragColor;

in vec3 shareColor;
in vec2 texCoord;

uniform sampler2D shareTexture;

void main(){
	FragColor = texture(shareTexture,texCoord)*vec4(shareColor,1.0);
}

上述的主要注意事项:sampler2D是一个GLSL针对纹理对象使用的内建数据类型,称为采样器,通过这个数据类型将纹理添加到片段着色器。

还有就是通过GLSL的内建函数texture来采样纹理的颜色,第一个参数就是纹理采样器,第二个参数就是对应的纹理座标。

7.纹理单元

纹理单元:一个纹理的位置值,可通过glUniformli函数给纹理采样器分配一个位置值,如此,可以在一个片段着色器中设置多个纹理。
通过把纹理单元赋值给采样器,可以一次性绑定多个纹理,通过后续的激活纹理手段就可以使用该纹理,多个纹理的综合效果,就可以做一些比较有趣的事情了。

激活绑定纹理

glActiveTexture(GL_TEXTURE0);//GL_TEXTURE0 是默认的 比如只有一个
glBindTexture(GL_TEXTURE_2D,texture0);


// 两个
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D,texture1);

//....16
glActiveTexture(GL_TEXTURE16);
glBindTexture(GL_TEXTURE_2D,texture15);

参考

learnopengl-cn

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章