OpenGL(四) 纹理贴图
几何模型&材质&纹理
渲染一个物体需要:
- 几何模型:决定了物体的形状
- 材质:绝对了当灯光照到上面时的作用效果
- 纹理:决定了物体的外观
纹理对象
纹理有2D的,有3D的。2D图像就是一张图片,3D图像是在渲染大气层等立体物体的时候会用到。此外多个2D纹理还可以组成Texture Array,如进行大地渲染的时候,需要沙砾,石头,杂草等多种纹理混合,就可以使用Texture Array进行存储,读取的时候加上index就可以读取出相应的纹理。
虽然被称为纹理,但是里面存储的数据也可以不是外观信息,如在进行天空渲染的时候,Precomputed Atmospheric Scattering算法需要与预计算并存储大气中点的通透度和散射值,就可以使用2D Texture存储大气中点的通透度,使用3DTexture存储大气中点的散射值。
即,纹理作为一种属性存储形式,根据需要有2D纹理和3D纹理,他们不仅可以存储外观属性,还可以存储描述物体的其他数据。
在渲染时,有些时候物理离相机很远,可能经过变换后,到显示中只占用极少的像素点,这几个像素点的值如何决定?
如果仍然使用采样方式,可能会出现相机稍微移动,采样对应的位置就大幅变化,效果就是这几个像素一直在抖动。
而且这么远的情况下,完全不需要使用相同精细度的纹理图像,可以节省很多空间。
为了适应不同远近情况下的纹理使用,提出了mipmap,即,存储不同精细程度的纹理,方便不同距离使用,这种思想和计算机视觉中的图像金字塔思想基本相同。其存储代价非常小,只占用了不到
4
3
\frac{4}{3}
34大小的存储空间【每次边长对半,存储为四分之一,级数求和】。
OpenGL中提供了一键生成mipmap的API
glGenerateMipmap(GL_TEXTURE_2D);
读取纹理涉及到图像解析,libpng、FreeImage、stb_image都是图像解析的开源库,这里使用stb_image,因为stb_image非常简单,只有头文件,但是相应的支持的图片格式也只有常用的几种。FreeImage支持的格式比较多,功能全面,但是相应的也复杂一些。
直接在此处下载stb_image.h文件,然后加入到自己的工程中,并新建一个cpp文件输入:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
使用stb_iamge的stbi_load
函数加载纹理图像文件并生成纹理:
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
stbi_image_free(data); //注意释放已经不用的图片
OpenGL每种纹理有多个纹理单元,默认纹理单元是0,且默认激活。可以通过激活别的纹理单元位置,来存储多个纹理,当需要使用多个纹理时很有用。
glActiveTexture(GL_TEXTURE0); // 在绑定纹理之前先激活纹理单元
//OpenGL规定至少16个纹理单元,可以通过GL_TEXTURE15访问,也可以通过GL_TEXTURE0 + 15来访问
glBindTexture(GL_TEXTURE_2D, texture);
采样器
模型大小通常与纹理大小是不一样的,需要进行采样,负责采样的被称为采样器。
采样方式
纹理坐标的范围通常是从(0, 0)到(1, 1),(u,v)代表%u,%v处的纹理值。
如果将uv值设到了这个范围以外,OpenGL默认的行为是重复这个纹理图像,但是也可以自定义
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
// 第一个表示要设置的纹理,第二个表示要设置的坐标轴,
// 第三个是策略,REPEAT就是重复,MIRRORED_REPEAT镜像重复,CLAMP_TO_EDGE边缘拉伸,CLAMP_TO_BORDER默认颜色
因为uv是浮点值,不一定对应某一个像素,尤其是在纹理较小,物体较大的时候。输入uv,如何计算出输出像素值,称为纹理过滤,OpenGL提供了多种不同选择,如:
- GL_NEAREST:选取临近像素的值输出
- GL_LINEAR:选择附近的像素进行线性插值,计算出的值作为结果。
设置方式:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 第一个选择设置的纹理,第二个表示要设置的情况,放大or缩小,第三个表示过滤模式。
根据上篇blog讲解的,渲染的流程中,在片段着色器中进行纹理渲染,所以片段着色器才是真正使用纹理的地方,GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler)。
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
FragColor = texture(ourTexture, TexCoord);
}
OpenGL中还内置了texture函数,第一个参数是采样器,第二个是纹理uv坐标点,输出就是该点对应的像素值。不用考虑采样器与纹理间的绑定,在进行glBindTexture
时就自动将纹理绑定到采样器上了。多个纹理单元时需要配多个采样器,会依次与采样器绑定。
混合
纹理体现的是对不同颜色光照的反射程度,光照体现的是不同光照的射入量,二者分量相乘得到的结果就是最终的显示效果。
多个纹理混合时,需要使用GLSL中内置的函数mix进行混合。
当没有纹理时,可以理解为纹理是(1,1,1,1),所以前面的三角形颜色变化,可以理解为光照颜色变化导致的,而三角形本身始终是不变的。只不过(1,1,1,1)的纹理表示全反射,光照没有损失全部反射出来了。如果纹理是渐变的,那么最终渲染出来的结果就也是不一样的
只有一个纹理的时候,其实约等于默认乘以了一个(1,1,1,1)的白光光照,如果想没有光照,需要乘以(0,0,0,1)
FragColor = texture(Texture1, TexCoord);
纹理和光照间需要分量相乘
FragColor = texture(Texture1, TexCoord) * timeColor;
当两个纹理的时候,要使用mix进行混合,
FragColor = mix(texture(Texture1, TexCoord),texture(Texture2, TexCoord),0.5) * timeColor;
参考
LearnOpenGL
LearnOpenGL CN
《计算机图形学编程(使用OpenGL和C++)第二版》