跳到内容

OpenGL 编程/现代 OpenGL 教程 06

来自维基教科书,开放的书籍,为开放的世界

加载纹理

[编辑 | 编辑源代码]
我们的纹理,在 2D 中

要加载纹理,我们需要代码以特定格式加载图像,如 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 缓冲区

[编辑 | 编辑源代码]

缓冲区基本上是图形卡内部的一个内存插槽,因此 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 部分

< OpenGL 编程

浏览和下载 完整代码
华夏公益教科书