跳到內容

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 程式設計

瀏覽和下載 完整程式碼
華夏公益教科書