OpenGL 编程/现代 OpenGL 教程 06
要加载纹理,我们需要代码以特定格式加载图像,如 JPEG 或 PNG。你的最终程序可能会使用诸如 SDL_Image、SFML 或 Irrlicht 等通用库,这些库支持各种图像格式,因此你无需编写自己的图像加载代码。
我们将使用 SDL2 的 SDL_Image 附加组件来加载纹理。
编辑你的头文件
/* Using SDL2_image to load PNG & JPG in memory */
#include "SDL_image.h"
以及你的 Makefile
CPPFLAGS=$(shell sdl2-config --cflags) $(shell $(PKG_CONFIG) SDL2_image --cflags) $(EXTRA_CPPFLAGS)
LDLIBS=$(shell sdl2-config --libs) $(shell $(PKG_CONFIG) SDL2_image --libs) -lGLEW $(EXTRA_LDLIBS)
EXTRA_LDLIBS?=-lGL
PKG_CONFIG?=pkg-config
all: cube
clean:
rm -f *.o cube
cube: ../common-sdl2/shader_utils.o
.PHONY: all clean
然后在 init_resources
中,我们可以
SDL_Surface* res_texture = IMG_Load("res_texture.png");
if (res_texture == NULL) {
cerr << "IMG_Load: " << SDL_GetError() << endl;
return false;
}
res_texture->pixels
现在包含来自 PNG 图像的未压缩像素。res_texture->format
包含有关它们如何存储的信息(RGB、RGBA...)。有关详细信息,请参阅 SDL_Surface 文档。
注意:你可以在代码库中找到 GIMP 源代码作为 res_texture.xcf。
缓冲区基本上是图形卡内部的一个内存插槽,因此 OpenGL 可以非常快地访问它。
我们现在不使用“mipmap”,因此请确保将 GL_TEXTURE_MIN_FILTER
指定为除默认基于 mipmap 的行为之外的任何值 - 在这种情况下,为线性插值。
为了简单起见,我们直接指定源格式,但理想情况下,我们应该检查 res_texture->format
并可能将其预先转换为 OpenGL 支持的格式。
/* Globals */
GLuint texture_id, program_id;
GLint uniform_mytexture;
/* init_resources */
SDL_Surface* res_texture = IMG_Load("res_texture.png");
if (res_texture == NULL) {
cerr << "IMG_Load: " << SDL_GetError() << endl;
return false;
}
glGenTextures(1, &texture_id);
glBindTexture(GL_TEXTURE_2D, texture_id);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, // target
0, // level, 0 = base, no minimap,
GL_RGBA, // internalformat
res_texture->w, // width
res_texture->h, // height
0, // border, always 0 in OpenGL ES
GL_RGBA, // format
GL_UNSIGNED_BYTE, // type
res_texture->pixels);
SDL_FreeSurface(res_texture);
我们在调用程序之前设置纹理 uniform(即使在这种情况 下,我们将其设置为插槽 0
)。
注意:mytexture
不是纹理 ID,而是我们绑定纹理 ID 的纹理单元插槽。
/* render */
glActiveTexture(GL_TEXTURE0);
glUniform1i(uniform_mytexture, /*GL_TEXTURE*/0);
glBindTexture(GL_TEXTURE_2D, texture_id);
/* free_resources */
glDeleteTextures(1, &texture_id);
现在我们需要说明每个顶点在我们纹理上的位置。为此,我们将用 texcoord
替换顶点着色器中的 v_color
属性
GLint attribute_coord3d, attribute_v_color, attribute_texcoord;
/* init_resources */
attribute_name = "texcoord";
attribute_texcoord = glGetAttribLocation(program, attribute_name);
if (attribute_texcoord == -1) {
cerr << "Could not bind attribute " << attribute_name << endl;
return false;
}
现在,我们把纹理的哪一部分映射到,比如,前面板的左上角?好吧,这取决于
- 对于前面板:纹理的左上角
- 对于顶面板:纹理的左下角
我们看到多个纹理点将附加到同一个顶点。顶点着色器将无法决定选择哪一个。
因此,我们需要通过为每个面使用 4 个顶点来重新编写立方体,而不重复使用顶点。
不过,首先,我们只处理前面板。简单!我们只需要显示前 2 个三角形(前 6 个顶点)即可
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);
因此,我们的纹理坐标在 [0, 1] 范围内,x 轴从左到右,y 轴从下到上
/* init_resources */
GLfloat cube_texcoords[] = {
// front
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
};
glGenBuffers(1, &vbo_cube_texcoords);
glBindBuffer(GL_ARRAY_BUFFER, vbo_cube_texcoords);
glBufferData(GL_ARRAY_BUFFER, sizeof(cube_texcoords), cube_texcoords, GL_STATIC_DRAW);
/* render */
glEnableVertexAttribArray(attribute_texcoord);
glBindBuffer(GL_ARRAY_BUFFER, vbo_cube_texcoords);
glVertexAttribPointer(
attribute_texcoord, // attribute
2, // number of elements per vertex, here (x,y)
GL_FLOAT, // the type of each element
GL_FALSE, // take our values as-is
0, // no extra data between each position
0 // offset of first element
);
顶点着色器
attribute vec3 coord3d;
attribute vec2 texcoord;
varying vec2 f_texcoord;
uniform mat4 mvp;
void main(void) {
gl_Position = mvp * vec4(coord3d, 1.0);
f_texcoord = texcoord;
}
片段着色器
varying vec2 f_texcoord;
uniform sampler2D mytexture;
void main(void) {
gl_FragColor = texture2D(mytexture, f_texcoord);
}
但发生了什么?我们的纹理上下颠倒了!
OpenGL 约定(原点在左下角)与 2D 应用程序中的约定(原点在左上角)不同。要解决这个问题,我们可以
- 从下到上读取像素行
- 交换像素行
- 交换纹理 Y 坐标
大多数图形库以 2D 约定返回像素数组。但是,DevIL 具有一个选项可以定位原点并避免此问题。或者,某些格式(如 BMP 和 TGA)本机存储从下到上的像素行(这可以解释为什么 TGA 格式在 3D 开发人员中如此流行),如果为它们编写自定义加载器,则很有用。
也可以在运行时在 C 代码中交换像素行。如果你使用 Python 等高级语言进行编程,这甚至可以在一行代码中完成。缺点是纹理加载会由于此额外的步骤而变得稍微慢一些。
反转纹理坐标是我们最简单的方法,我们可以在片段着色器中执行此操作
void main(void) {
vec2 flipped_texcoord = vec2(f_texcoord.x, 1.0 - f_texcoord.y);
gl_FragColor = texture2D(mytexture, flipped_texcoord);
}
好的,从技术上讲,我们本来可以在一开始就以相反的方向编写纹理坐标 - 但其他 3D 应用程序倾向于以我们描述的方式工作。
正如我们所讨论的,我们为每个面指定独立的顶点
GLfloat cube_vertices[] = {
// front
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
// top
-1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, -1.0,
-1.0, 1.0, -1.0,
// back
1.0, -1.0, -1.0,
-1.0, -1.0, -1.0,
-1.0, 1.0, -1.0,
1.0, 1.0, -1.0,
// bottom
-1.0, -1.0, -1.0,
1.0, -1.0, -1.0,
1.0, -1.0, 1.0,
-1.0, -1.0, 1.0,
// left
-1.0, -1.0, -1.0,
-1.0, -1.0, 1.0,
-1.0, 1.0, 1.0,
-1.0, 1.0, -1.0,
// right
1.0, -1.0, 1.0,
1.0, -1.0, -1.0,
1.0, 1.0, -1.0,
1.0, 1.0, 1.0,
};
对于每个面,顶点按逆时针方向添加(当观察者面向该面时)。因此,纹理映射对所有面都将相同
GLfloat cube_texcoords[2*4*6] = {
// front
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
};
for (int i = 1; i < 6; i++)
memcpy(&cube_texcoords[i*4*2], &cube_texcoords[0], 2*4*sizeof(GLfloat));
在这里,我们指定了前面板的映射,并在其余 5 个面上复制了它。
如果一个面是顺时针而不是逆时针方向,那么纹理将被镜像显示。没有关于方向的约定,你只需要确保纹理坐标正确映射到顶点即可。
立方体元素也以类似的方式编写,其中包含 2 个三角形,其索引为 (x, x+1, x+2),(x+2, x+3, x)
/* init_resources */
GLushort cube_elements[] = {
// front
0, 1, 2,
2, 3, 0,
// top
4, 5, 6,
6, 7, 4,
// back
8, 9, 10,
10, 11, 8,
// bottom
12, 13, 14,
14, 15, 12,
// left
16, 17, 18,
18, 19, 16,
// right
20, 21, 22,
22, 23, 20,
};
/* render */
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_cube_elements);
int size; glGetBufferParameteriv(GL_ELEMENT_ARRAY_BUFFER, GL_BUFFER_SIZE, &size);
glDrawElements(GL_TRIANGLES, size/sizeof(GLushort), GL_UNSIGNED_SHORT, 0);
为了增加乐趣,并检查底面,让我们在 logic
中实现 NeHe 的飞行立方体教程中展示的 3 个旋转运动
float angle = SDL_GetTicks() / 1000.0 * 15; // base 15° per second
glm::mat4 anim = \
glm::rotate(glm::mat4(1.0f), glm::radians(angle)*3.0f, glm::vec3(1, 0, 0)) * // X axis
glm::rotate(glm::mat4(1.0f), glm::radians(angle)*2.0f, glm::vec3(0, 1, 0)) * // Y axis
glm::rotate(glm::mat4(1.0f), glm::radians(angle)*4.0f, glm::vec3(0, 0, 1)); // Z axis
我们完成了!
- stb_image: 单头文件,公共领域,图像加载库;不过,这些不是官方的 PNG、JPG 等实现,并且有一些(已记录的)限制;SFML 使用它
- SOIL (Simple OpenGL Image Library): 公共领域,专为 OpenGL 设计的图像加载库;最后一个版本是在 2008 年发布的,维护人员没有回复 Android 修补程序
- 纹理 在传统 OpenGL 1.x 部分