加载纹理图像文件
- 用于保存纹理图像的纹理对象(在本章中我们仅考虑 2D 图像);
- 特殊的统一采样器变量,以便顶点着色器访问纹理;
- 用于保存纹理坐标的缓冲区;
- 用于将纹理坐标传递给管线的顶点属性;
- 显卡上的纹理单元。
使用SOIL2读取纹理
封装读取代码
GLuint loadTexture(const char *texImagePath) {
GLuint textureID;
textureID = SOIL_load_OGL_texture(texImagePath,
SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, SOIL_FLAG_INVERT_Y);
if (textureID == 0) cout << "could not find texture file" << texImagePath << endl;
return textureID;
}
纹理坐标
纹理坐标是对纹理图像(通常是 2D 图像)中的像素的引用。
2D 纹理坐标最为常见(OpenGL 确实支持其他一些维度,但本章不会介绍它们)。2D 纹理图像被设定为矩形,左下角的位置坐标为(0,0),右上角的位置坐标为(1,1)。理想情况下,纹理坐标应该在[0, 1]区间内取值。
构建纹理对象
GLuint brickTexture = Utils::loadTexture("brick1.jpg");
构建纹理坐标
思考如何将纹理的坐标和几何体的顶点相对应,并构造
将纹理坐标载入缓冲区
以用与前面加载顶点相似的方式将纹理坐标加载到 VBO 中。所以会有一个顶点VBO,一个纹理坐标VBO。
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(pyrTexCoords), pyrTexCoords, GL_STATIC_DRAW)
在着色器中使用纹理:采样器变量和纹理单元
声明一个采样器变量
layout (binding=0) uniform sampler2D samp;
激活纹理单元并将其绑定到特定的纹理对象
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, brickTexture);
修改片段着色器输出颜色的方式
in vec2 tc; // 纹理坐标
...
color = texture(samp, tc);
纹理贴图:示例程序
#include <GL\glew.h>
#include <GLFW\glfw3.h>
#include <SOIL2\soil2.h>
#include <string>
#include <iostream>
#include <fstream>
#include <cmath>
#include <glm\glm.hpp>
#include <glm\gtc\type_ptr.hpp> // glm::value_ptr
#include <glm\gtc\matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale, glm::perspective
#include "Utils.h"
using namespace std;
#define numVAOs 1
#define numVBOs 2
float cameraX, cameraY, cameraZ;
float pyrLocX, pyrLocY, pyrLocZ;
GLuint renderingProgram;
GLuint vao[numVAOs];
GLuint vbo[numVBOs];
// variable allocation for display
GLuint mvLoc, projLoc;
int width, height;
float aspect;
glm::mat4 pMat, vMat, mMat, mvMat;
GLuint brickTexture;
void setupVertices(void) {
float pyramidPositions[54] =
{
-1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 0.0f, //front
1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 0.0f, 1.0f, 0.0f, //right
1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 0.0f, 1.0f, 0.0f, //back
-1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 0.0f, //left
-1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, //LF
1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f //RR
};
float textureCoordinates[36] =
{
0.0f, 0.0f, 1.0f, 0.0f, 0.5f, 1.0f,
0.0f, 0.0f, 1.0f, 0.0f, 0.5f, 1.0f,
0.0f, 0.0f, 1.0f, 0.0f, 0.5f, 1.0f,
0.0f, 0.0f, 1.0f, 0.0f, 0.5f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f
};
glGenVertexArrays(1, vao);
glBindVertexArray(vao[0]);
glGenBuffers(numVBOs, vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(pyramidPositions), pyramidPositions, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(textureCoordinates), textureCoordinates, GL_STATIC_DRAW);
}
void init(GLFWwindow* window) {
renderingProgram = Utils::createShaderProgram("vertShader.glsl", "fragShader.glsl");
cameraX = 0.0f; cameraY = 0.0f; cameraZ = 4.0f;
pyrLocX = 0.0f; pyrLocY = 0.0f; pyrLocZ = 0.0f;
setupVertices();
glfwGetFramebufferSize(window, &width, &height);
aspect = (float)width / (float)height;
pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
brickTexture = Utils::loadTexture("brick1.jpg");
// SEE Utils.cpp, the "loadTexture()" function, the code before the mipmapping section
}
void display(GLFWwindow* window, double currentTime) {
glClear(GL_DEPTH_BUFFER_BIT);
glClearColor(0.0, 0.0, 0.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(renderingProgram);
mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix");
projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");
vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));
mMat = glm::translate(glm::mat4(1.0f), glm::vec3(pyrLocX, pyrLocY, pyrLocZ));
mMat = glm::rotate(mMat, -0.45f, glm::vec3(1.0f, 0.0f, 0.0f));
mMat = glm::rotate(mMat, 0.61f, glm::vec3(0.0f, 1.0f, 0.0f));
mMat = glm::rotate(mMat, 0.00f, glm::vec3(0.0f, 0.0f, 1.0f));
mvMat = vMat * mMat;
glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(1);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, brickTexture);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glDrawArrays(GL_TRIANGLES, 0, 18);
}
void window_size_callback(GLFWwindow* win, int newWidth, int newHeight) {
aspect = (float)newWidth / (float)newHeight;
glViewport(0, 0, newWidth, newHeight);
pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
}
int main(void) {
if (!glfwInit()) {
exit(EXIT_FAILURE); }
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter5 - program1", NULL, NULL);
glfwMakeContextCurrent(window);
if (glewInit() != GLEW_OK) {
exit(EXIT_FAILURE); }
glfwSwapInterval(1);
glfwSetWindowSizeCallback(window, window_size_callback);
init(window);
while (!glfwWindowShouldClose(window)) {
display(window, glfwGetTime());
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
exit(EXIT_SUCCESS);
}
多级渐远纹理贴图 mipmapping
纹理贴图经常会在渲染图像中导致各种不利的伪影。这是因为纹理图像的分辨率或长宽比很少与被纹理贴图的场景中区域的分辨率或长宽比匹配。
当图像分辨率小于所绘制区域的分辨率时,会出现一种很常见的伪影。在这种情况下,需要拉伸图像以覆盖整个区域,这样图像就会变得模糊(并且可能变形)。根据纹理的性质,有时可以通过改变纹理坐标分配方式来应对这种情况,使得纹理需要较少的拉伸。另一种解决方案是使用更高分辨率的纹理图像。
相反的情况是图像纹理的分辨率大于被绘制区域的分辨率。可能并不是很容易理解为什么这会造成问题,但问题确实会出现!在这种情况下,可能会出现明显的叠影,从而产生奇怪的错误图案,或移动物体中的“闪烁”效果。
使用多级渐远纹理贴图(mipmapping)技术可以在很大程度上校正这一类的采样误差伪影,它需要用各种分辨率创建纹理图像的不同版本。
在 OpenGL 中,可以通过将 GL_TEXTURE_MIN_FILTER 参数设置为所需的缩小方法来选择多级渐远纹理的采样方法。有多种可选方法(NEAREST,线性过滤,双线性,三线性)
//Utils::loadTexture()
glBindTexture(GL_TEXTURE_2D, textureID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glGenerateMipmap(GL_TEXTURE_2D);
各向异性过滤
标准的多级渐远纹理贴图以各种正方形分辨率(如 256 像素×256 像素、128 像素×128 像素等)对纹理图像进行采样,而各向异性过滤却以多种矩形分辨率对纹理进行采样(如 256 像素×128 像素、64像素×128 像素等)。
各向异性过滤比标准多级渐远纹理贴图的计算代价更高,并且不是 OpenGL 的必需部分。
// 如果还使用各向异性过滤
if (glewIsSupported("GL_EXT_texture_filter_anisotropic")) {
GLfloat anisoSetting = 0.0f;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &anisoSetting);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, anisoSetting);
}
环绕和平铺
OpenGL 实际上支持任何取值范围的纹理坐标。有几个选项可以用来指定当纹理坐标超出[0, 1]区间时会发生什么,可以使用glTexParameteri()设置。
- GL_REPEAT:重复,即忽略纹理坐标的整数部分,生成重复或“平铺”图案。这是默认行为。
- GL_MIRRORED_REPEAT:镜像重复,即忽略纹理坐标的整数部分,但是当整数部分为奇数时反转坐标,因此重复的图案在原图案和其镜像图案之间交替。
- GL_CLAMP_TO_EDGE:夹紧到边缘,即将小于 0 的坐标和大于 1 的坐标分别设置为 0 和 1。
- GL_CLAMP_TO_BORDER:夹紧到边框,即将[0, 1]以外的纹元设置成指定的边框颜色。
透视变形
考虑一个由两个三角形组成的矩形,纹理贴图是棋盘图样,面向相机。当矩形围绕 x 轴旋转时,矩形的顶部会倾斜并远离相机,而矩形的下半部分则更靠近相机。因此,我们希望顶部的方块变小,底部的方块变大。但是,纹理坐标的线性插值将导致所有正方形的高度相等。沿着构成矩形的两个三角形接缝处的对角线加剧失真。
使用透视失真的算法,并且默认情况下,OpenGL 在栅格化期间会应用透视校正算法
可以通过在包含纹理坐标的顶点属性的声明中添加关键字“noperspective”来禁用 OpenGL 的透视校正
//vertex
noperspective out vec2 texCoord;
//frag
noperspective in vec2 texCoord;
材质——更多 OpenGL 细节
在没有纹理加载库(如 SOIL2)的情况下加载和使用纹理时需要了解的一些细节。
- 使用 C++工具读取图像文件数据。
- 生成 OpenGL 纹理对象。
- 将图像文件数据复制到纹理对象中。
GLuint textureID; // 如果需要创建多于一个纹理对象,则使用 GLuint 类型的数组
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID)
glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, width, height, 0, GL_BGR,GL_UNSIGNED_BYTE, data);
补充说明
- 纹理单元的许多用途
- 改变反射光线
- 存储高度图生成地形
- 存储阴影贴图为场景添加阴影
- 着色器允许向纹理写入数据,允许着色器修改纹理图像
- 更多图像修复工具,如全屏抗锯齿、超采样。
- 采样器对象
习题
纹理坐标顶点属性中添加 noperspective 声明
没有 noperspective 的贴图看起来更合理
用自己的图来做纹理贴图
把这个函数换成自己的
brickTexture = Utils::loadTexture("head.jpg");
自己的星球纹理
根据上述添加纹理需要的操作,进行添加逻辑
//纹理采样坐标
float textureCoordinates[36] =
{
0.0f, 0.0f, 1.0f, 0.0f, 0.5f, 1.0f,
0.0f, 0.0f, 1.0f, 0.0f, 0.5f, 1.0f,
0.0f, 0.0f, 1.0f, 0.0f, 0.5f, 1.0f,
0.0f, 0.0f, 1.0f, 0.0f, 0.5f, 1.0f,
0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f
};
//设置纹理采样坐标缓冲区
glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);
glBufferData(GL_ARRAY_BUFFER, sizeof(textureCoordinates), textureCoordinates, GL_STATIC_DRAW);
//读取纹理
brickTexture1 = Utils::loadTexture("sun.jpg");
brickTexture2 = Utils::loadTexture("moon.jpg");
brickTexture3 = Utils::loadTexture("earth.jpg");
//绑定采样器
glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(1);
//激活纹理坐标
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, brickTexture1);
着色器和纹理相关的代码
//verShader.glsl
layout (location=1) in vec2 texCoord;
out vec2 tc;
layout (binding=0) uniform sampler2D samp;
void main(void){
tc = texCoord;
}
//fragShader.glsl
in vec2 tc;
layout (binding=0) uniform sampler2D samp;
void main(void)
{
color = texture(samp, tc);
}
疑惑
- OpenGL为什么总要给各种对象设置激活状态?
- 基于“状态机”模型的设计理念
- 明确告诉OpenGL当前要操作的是哪个对象,同时减少性能损耗和上下文切换
- 常见的激活:着色器、缓冲区、纹理
- 纹理坐标在光栅着色器进行插值,是依据当前坐标计算出插值使用,还是再回去根据插值采样纹理
- 使用插值后的纹理坐标来进行纹理采样
- 片元着色器在光栅化阶段之后
- 纹理单元是什么?它起着什么作用?
- 纹理单元本质上是GPU硬件中负责纹理采样和处理的单元。
- 纹理单元的作用是将纹理图像与3D模型的表面坐标进行映射,计算最终的颜色值
- 为什么需要在顶点着色器和片元着色器都声明采样器?
- uniform 变量是“全局共享的”
- 确保程序的结构完整性,片段着色器才是实际使用纹理进行计算的地方
- layout (binding=0) 和 layout (location=0) 的区别是什么?
binding
主要与 uniform 资源(如纹理、缓冲区等)的绑定有关,而location
主要与顶点属性的输入和输出位置相关。
- 光栅化线性插值的算法是怎样的?
其他
如何将程序绑定着色器的uniform变量
GLuint offsetLoc; //声明对象
offsetLoc = glGetUniformLocation(renderingProgram, "offset"); //获得着色器变量的引用
glProgramUniform1f(renderingProgram, offsetLoc, x); //将程序变量的值输出给着色器变量
glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
着色器绑定数据的细节整理
绑定坐标数据信息
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);//0代表location=0,3属性大小。详细参数细节可询问AI
glEnableVertexAttribArray(0); //启用顶点属性数组,使得该顶点属性在着色器中能够被使用。
- 绑定 VBO:将顶点数据缓冲区(
vbo[0]
)绑定为当前的GL_ARRAY_BUFFER
。 - 设置顶点属性指针:指定如何从缓冲区读取顶点数据,告诉 OpenGL 顶点属性的位置、数据类型、步长等。
- 启用顶点属性数组:启用指定位置的顶点属性数组,使得顶点数据能够传递到顶点着色器进行处理。
绑定采样器
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, brickTexture);
glActiveTexture(GL_TEXTURE0)
:激活纹理单元 0,准备在后续操作中使用这个纹理单元。glBindTexture(GL_TEXTURE_2D, brickTexture)
:将brickTexture
绑定到当前激活的纹理单元 0 上,之后你就可以在着色器中通过sampler2D
等变量访问这个纹理。