iT邦幫忙

2021 iThome 鐵人賽

DAY 5
1
自我挑戰組

翻車機率極高的2D平台遊戲(2D Platformer)製作系列 第 5

[Day5] 第一章貼圖

今日目標

  • 載入圖片,畫出第一張圖

stb_image.h

第三天-驅動OpenGL這篇有稍稍提到這個玩意

Single-header file

並推薦了stb這個在github上的repo。這個repo放的是作者Sean T. Barrett的撰寫C語言時的各種工具。這些工具最大的特點就是把實作跟宣告寫在同一個文件,利用macro分割開來,這樣可以解決C與C++中麻煩的引用問題。

專案中要使用stb_image.h是一個「使用起來」簡單,載入處理一些常見的圖檔的函式庫。

老樣子,我把這個文件放在external資料夾裡面,再額外包一層stb的資料夾。然後就可以開始載入圖片了。

載入圖片

圖片我用的是Kenny上的Animal Pack Redux,這是一個很讚的網站,裡面提供了許多免費的素材~

首先,定義一個結構(struct)把要的資料記錄下來

// iron_types.h
typedef enum GL_TextureFmt{
    TEX_FMT_WHITE_BLACK = GL_RED,
    TEX_FMT_GRAYSCALE   = GL_RG,
    TEX_FMT_RGB         = GL_RGB,
    TEX_FMT_RGBA        = GL_RGBA,
} TextureFmt;

typedef struct Texture {
    unsigned int id;
    int w, h;
    TextureFmt gl_fmt;
} Texture;

然後,我還定義了一個enum,表示Texture的對應在OpenGL裡面的格式,其實可以點實作進去看,其實就是OpenGL,定義的一系列的數字,用於識別圖片格式。

再來就是載入了...

// iron_asset.c - LoadTextureFile

    int channels;
	unsigned char* pixels = stbi_load(file_name, &texture->w, &texture->h, &channels, 0);
	if (pixels == NULL) {
		stbi_image_free(pixels);
		printf("Failed to load image: %s\n", file_name);
		return RES_ERROR_LOAD_IMAGE_FILE;
	}

	int src_img_fmt;
	if (channels == 2) {
		texture->gl_fmt = TEX_FMT_GRAYSCALE;
		src_img_fmt = GL_RG8;
	} else if (channels == 3) {
		texture->gl_fmt = TEX_FMT_RGB;
		src_img_fmt = GL_RGB8;
	} else if (channels == 4) {
		texture->gl_fmt = TEX_FMT_RGBA;
		src_img_fmt = GL_RGBA8;
	} else {
		texture->gl_fmt = TEX_FMT_WHITE_BLACK;
		src_img_fmt = GL_R8;
	}
    // continue...

有用到了就兩個,stbi_loadstbi_freestbi可以視為他的命名空間。

stbi_load最後會回傳圖片所有的像素點,存成unsigned char的陣列,可以看到的第三個參數我代入channels,這個就是圖片的顏色通道,例如PNG有RGBA四個通道,那channels就會拿到「4」,最後在對應到OpenGL的格式。除了OpenGL的格式,我還存了圖片的「原」格式,下面會用到

再來就是設置Texture,把他儲存在OpenGL的狀態機裡面:

   // iron_asset.c - LoadTextureFile 
   
   // part 1
    glBindTexture(GL_TEXTURE_2D, 0);

	glGenTextures(1, &texture->id);
	glBindTexture(GL_TEXTURE_2D, texture->id);
   // part 2
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
   // part 3
	glTexImage2D(GL_TEXTURE_2D, 0, src_img_fmt, texture->w, texture->h, 0, texture->gl_fmt, GL_UNSIGNED_BYTE, pixels);
   // part 4
	glBindTexture(GL_TEXTURE_2D, 0);

這裡分作4個Part講解比較容易

Part1: 生成與綁定新的Texture,第一行的bind texture是為了防止之後,有正在使用的texture,被蓋到才加上的
Part2: 設置Texture的環繞(?)與過濾(?)的方式,老實說我不知道中文怎麼翻譯,建議可以看看這個可以更加清楚。
Part3: 設置資料到OpenGL裡,第2個參數必填0 參考
Part4: 設置完成,解除綁定

這樣就把圖片設置在OpenGL裡囉~之後可以通過Texture.id來呼叫。

畫出來吧~

之前寫在iron_render.c內部的shader code,需要加上兩個參數
1.Texture的座標
2.Texture本身

這是更新後的樣子:

static const char* DEFAULT_2D_VERTEX_SHADER_CODE = "#version 330 core\n"
"in vec2 _Pos;\n"
"in vec2 _Texcoords;\n" // 1.
"out vec2 Texcoords;\n" // 2.
"void main() {\n"
"   gl_Position = vec4(_Pos, 0.0, 1.0);\n"
"   Texcoords = _Texcoords;\n" // 3.
"}\n\0";

static const char* DEFAULT_2D_FRAGMENT_SHADER_CODE = "#version 330 core\n"
"in vec2 Texcoords;\n" // 4.
"uniform vec4 _Color;\n"
"uniform sampler2D _Texture2D;" // 5.
"out vec4 _FragColor;\n"
"void main() {\n"
"   _FragColor = _Color * texture(_Texture2D, Texcoords);\n" // 6.
"}\n\0";

然後更新iron_render模組內,預設的shader,在iron_asset.LoadShaderCode的地方加上這個


    // texture coordinate
    int texcoord_location = glGetAttribLocation(shader->id, "_Texcoords");
	if (texcoord_location < 0) {
		return RES_ERROR_GET_SHADER_ATTRIBUTE;
	}
	shader->attribs_locations[SHADER_ATTRIB_VEC2_TEXCOORD] = texcoord_location;
    
    // texture
    int texture_location = glGetUniformLocation(shader->id, "_Texture2D");
	if (texture_location < 0) {
		return RES_ERROR_GET_SHADER_ATTRIBUTE;
	}
	shader->attribs_locations[SHADER_ATTRIB_SAMPLER2D_TEXTURE] = texcoord_location;

P.S. 寫的時候發現不應該沒有找到這個屬性就Return,不然之後有不同需求的Shader,裡面沒有包含這屬性就GG了。

最後...利用之前DrawFirstRectangle的code,改造一下,就可以畫出我們第一個圖片了

    unsigned int vbo, vao, ibo;

	glGenVertexArrays(1, &vao);
	glBindVertexArray(vao);

	glGenBuffers(1, &vbo);
	glBindBuffer(GL_ARRAY_BUFFER, vbo);
	glBufferData(GL_ARRAY_BUFFER, sizeof(VERTICES), VERTICES, GL_STATIC_DRAW);

	glGenBuffers(1, &ibo);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(INDICES), INDICES, GL_STATIC_DRAW);

	// set position attribute
	glVertexAttribPointer(RENDER_2D_CONTEXT.default_shader.attribs_locations[SHADER_ATTRIB_VEC2_POS], 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(RENDER_2D_CONTEXT.default_shader.attribs_locations[SHADER_ATTRIB_VEC2_POS]);

	// set texture coordinate attribute
	glVertexAttribPointer(RENDER_2D_CONTEXT.default_shader.attribs_locations[SHADER_ATTRIB_VEC2_TEXCOORD], 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
    glEnableVertexAttribArray(RENDER_2D_CONTEXT.default_shader.attribs_locations[SHADER_ATTRIB_VEC2_TEXCOORD]);

	glUseProgram(RENDER_2D_CONTEXT.default_shader.id);

	// set shader color
	V4f v = ColorToVec4f(c);
	glUniform4f(RENDER_2D_CONTEXT.default_shader.attribs_locations[SHADER_ATTRIB_VEC4_COLOR], v.r, v.g, v.b, v.a);

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, texture->id);

	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

	glBindTexture(GL_TEXTURE_2D, 0);
	glBindVertexArray(0);

有個注意一下,原本的VERTICES,除了頂點位置,我也把Texture的座標放進去了,所以原本的一組頂點是2個float會變成4個float,中間還需要告訴glVertexAttribPointer頂點與貼圖座標的間距。

完成!但...怎麼怪怪的

完成之後,我的輸出是這樣,發福的倒立兔子

原因有幾個

  1. 圖片沒有翻轉,這個可以在載入圖片之前加上stbi_set_flip_vertically_on_load翻轉圖片
  2. 目前的座標系,是固定的,並不是依照圖原本的坐標系去處理的,預計會在座標轉換那邊在處理
  3. Alpha為0的地方被畫出來了,這個是沒有把GL_BLEND打開,可以在CreateRenderer的地方補上

// ...
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// ...

今天就先這樣,明天預計會寫輸入操作時間處理的部分

參考

今日的成果


上一篇
[Day4] 第一個矩形
下一篇
[Day6] 初見輸入系統
系列文
翻車機率極高的2D平台遊戲(2D Platformer)製作33

尚未有邦友留言

立即登入留言