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

cpp 复制代码
glGenerateMipmap(GL_TEXTURE_2D);

读取纹理涉及到图像解析,libpng、FreeImage、stb_image都是图像解析的开源库,这里使用stb_image,因为stb_image非常简单,只有头文件,但是相应的支持的图片格式也只有常用的几种。FreeImage支持的格式比较多,功能全面,但是相应的也复杂一些。

直接在此处下载stb_image.h文件,然后加入到自己的工程中,并新建一个cpp文件输入:

cpp 复制代码
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

使用stb_iamge的stbi_load函数加载纹理图像文件并生成纹理:

cpp 复制代码
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,且默认激活。可以通过激活别的纹理单元位置,来存储多个纹理,当需要使用多个纹理时很有用。

cpp 复制代码
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默认的行为是重复这个纹理图像,但是也可以自定义

cpp 复制代码
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:选择附近的像素进行线性插值,计算出的值作为结果。

设置方式:

cpp 复制代码
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 第一个选择设置的纹理,第二个表示要设置的情况,放大or缩小,第三个表示过滤模式。

根据上篇blog讲解的,渲染的流程中,在片段着色器中进行纹理渲染,所以片段着色器才是真正使用纹理的地方,GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler)。

cpp 复制代码
#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)

cpp 复制代码
FragColor = texture(Texture1, TexCoord);

纹理和光照间需要分量相乘

cpp 复制代码
FragColor = texture(Texture1, TexCoord) * timeColor;

当两个纹理的时候,要使用mix进行混合,

cpp 复制代码
FragColor = mix(texture(Texture1, TexCoord),texture(Texture2, TexCoord),0.5) * timeColor;

参考

LearnOpenGL
LearnOpenGL CN

《计算机图形学编程(使用OpenGL和C++)第二版》

相关推荐
凌云行者3 天前
OpenGL入门004——使用EBO绘制矩形
c++·cmake·opengl
闲暇部落3 天前
Android OpenGL ES详解——模板Stencil
android·kotlin·opengl·模板测试·stencil·模板缓冲·物体轮廓
阿赵3D4 天前
Unity引擎材质球残留贴图引用的处理
unity·材质·贴图
小彭努力中5 天前
114. 精灵模型标注场景(贴图)
前端·3d·webgl·贴图
小彭努力中5 天前
109. 工厂光源(环境贴图和环境光)
前端·深度学习·3d·webgl·贴图
凌云行者6 天前
OpenGL入门003——使用Factory设计模式简化渲染流程
c++·cmake·opengl
凌云行者6 天前
OpenGL入门002——顶点着色器和片段着色器
c++·cmake·opengl
Raina_H6 天前
Blender进阶:贴图与UV
经验分享·3d·blender·贴图·uv
Raina_H6 天前
Blender进阶:着色器节点
3d·blender·材质·贴图·着色器·uv
闲暇部落7 天前
Android OpenGL ES详解——裁剪Scissor
android·kotlin·opengl·窗口·裁剪·scissor·视口