如果想要特別讀取 BMP、JPEG 或 PNG 檔案,通常都會需要撰寫 Reder,但除了 BMP 之外其他的格式讀取或寫入都較為複雜,所以說一般建議使用已經開發好的函式庫即可。
- 首先下載
stb_image.h
到專案根目錄,Github 載點。 - 之後創建一個新的
.cpp
檔案,名稱隨意,裡面內容如下:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
- 在要使用的檔案中(例如
main.cpp
)引入標頭檔,即可開始使用 stb 圖片函式庫了!
#include "stb_image.h"
- 首先輸入指令來安裝
stb
函式庫。
$ vcpkg install stb --triplet=x64-windows
- 安裝好後,只需要在專案根目錄下的
CMakeLists.txt
加上相關設定
find_path(STB_INCLUDE_DIRS "stb.h")
target_include_directories(目標名稱 PRIVATE ${STB_INCLUDE_DIRS})
- 之後創建一個新的
.cpp
檔案,名稱隨意,裡面內容如下:
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
- 在要使用的檔案中(例如
main.cpp
)引入標頭檔,即可開始使用 stb 圖片函式庫了!
#include <stb_image.h>
讀取圖片的方式,就是這麼如此簡單:
int width, height, nrChannels;
unsigned char *image = stbi_load("圖片位置.png", &width, &height, &nrChannels, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
由於 OpenGL 的 Texture Coordinate 中,原點 (0, 0)
是在左下角,而一般來說圖片索引的 (0, 0)
會是左上角(想想陣列的概念),所以讀取出來的圖片會是上下顛倒的,這個時候可以透過下列函式來矯正它:
stbi_set_flip_vertically_on_load(true);
記得要在呼叫
stbi_load()
之前。
除了使用 stb_image.h
之外,還可以使用 SDL2 自己的衍生函式庫 SDL2-Image
來實現讀取圖片的功能。
- 首先輸入指令來安裝
sdl2-image
函式庫。
$ vcpkg install sdl2-image[core,libjpeg-turbo] --triplet=x64-windows
- 安裝好後,記得要去專案根目錄下的
CMakeLists.txt
做相關設定:
find_package(sdl2-image CONFIG REQUIRED)
target_link_libraries(目標名稱 PRIVATE SDL2::SDL2_image)
- 接著引入標頭檔並且初始化,這樣就可以開始使用
SDL2-Image
了。
#include <SDL_image.h>
int main(int argc, char **argv) {
// ... SDL 初始化
// ... SDL Image 初始化
int sdl_img_init_flags = IMG_INIT_JPG | IMG_INIT_PNG;
int img_init = IMG_Init(sdl_img_init_flags);
if ((img_init & sdl_img_init_flags) != sdl_img_init_flags) {
std::cout << "Oops! Failed to initialize SDL Image extension library. :(\n" << SDL_GetError() << std::endl;
return -1;
}
// 其他程式 ....
}
讀取圖片的方式,就是這麼如此簡單:
SDL_Surface* image = IMG_Load("圖片位置.png");
// 抓取圖片寬、高與通道
int width = image->w;
int height = image->h;
int nrChannels = image->format->BitsPerPixel / 8;
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image->pixels);
很可惜的是,SDL2-Image
沒有提供垂直翻轉的函式可以呼叫,所以這邊只能自己寫函式去翻轉圖片,或者是另一個方法..,把 Texture Coordinate 的 Y 顛倒過來(雖然是最簡單但是不建議)。
void flip_surface(SDL_Surface* surface) {
SDL_LockSurface(surface);
int pitch = surface->pitch; // row size
char* temp = new char[pitch]; // intermediate buffer
char* pixels = (char*) surface->pixels;
for(int i = 0; i < surface->h / 2; ++i) {
// get pointers to the two rows to swap
char* row1 = pixels + i * pitch;
char* row2 = pixels + (surface->h - i - 1) * pitch;
// swap rows
memcpy(temp, row1, pitch);
memcpy(row1, row2, pitch);
memcpy(row2, temp, pitch);
}
delete[] temp;
SDL_UnlockSurface(surface);
}
// 上下翻轉圖片
SDL_Surface* image = IMG_Load("assets/textures/rickroll.png");
flip_surface(image);
當你有開始有讀取外部圖片的需求時,就會發現圖片路徑是一個很大的問題,比如說你在主程式寫說要讀取 assets\textures\doge.png
(相對路徑寫法),但編譯後執行程式卻說一直找不到該檔案,除非你改成絕對路徑寫法,比如 C:\uers\user\Desktop\CGHomework05\assets\textures\doge.png
才可以讀取,但問題是這樣的寫法很不明智,因為代表就已經寫死你的專案必須要放在桌面,一旦改變了路徑或是換了地方又整個要重改。
之所以會有這樣的原因,就是因為你認為的【相對】路徑中的相對是基於 main.cpp
,而事實上不是,是【相對於你最終編譯出來的執行檔】,舉例來說使用 Visual Studio 編譯的話,最終的程式(.exe
)會產生在路徑 CGHomework05\out\build\x64-Debug
當中,所以說在 main.cpp
所寫的圖片路徑(假設是 assets\textures\doge.png
),程式的路徑反而是會抓取 CGHomework05\out\build\x64-Debug\assets\textures\doge.png
,而非我們預期的 CGHomework05\assets\textures\doge.png
。
或許會有人就乾脆把在專案根目錄(CGHomework05
)下的外部資源檔案全部手動複製到建置目錄下(CGHomework05\out\build\x64-Debug\
),雖然可以解決方法但一樣換湯不換藥,畢竟建置目錄內的東西可是會隨著不同電腦、不同環境、不同 IDE、不同建置器和不同編譯器就會有所不一樣,所以說強烈不建議這樣做。
最好的解法就是透過 CMake 來自動幫我們處理,只要輸入以下語法即可:
add_custom_command(TARGET 目標名稱 POST_BUILD
COMMAND ${CMAKE_COMMAND} -E create_symlink
"${CMAKE_CURRENT_SOURCE_DIR}/assets"
"$<TARGET_FILE_DIR:目標名稱>/assets"
DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/assets" # Make sure the directory exists
COMMENT "Creating symlink from build tree to project resources..."
VERBATIM
)
add_custom_command(TARGET <target>
PRE_BUILD | PRE_LINK | POST_BUILD
COMMAND command1 [ARGS] [args1...]
[COMMAND command2 [ARGS] [args2...] ...]
[BYPRODUCTS [files...]]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[VERBATIM] [USES_TERMINAL]
[COMMAND_EXPAND_LISTS])
add_custom_command()
代表是我們可以透過該函式來執行一些自定義的指令,來達成一些我們可能會需要的事情,大概就式輸入指令到終端機的概念。POST_BUILD
意思是【建置後事件】,也就說當程式建置好後才會觸發指令,除此之外還有PRE_BUILD
和PRE_LINK
。COMMAND
就是用來執行命令的意思。DEPENDS
就是依賴條件,當某檔案不存在時將會出錯。VERBATIM
大概就是命令的所有參數都將為構建工具正確轉義,總之 CMake 官方文件建議使用。
而上述指令大致上的意思就是說,在程式建置好後會輸入以下指令:
$ cmake -E create_symlink "${CMAKE_CURRENT_SOURCE_DIR}/assets" "$<TARGET_FILE_DIR:${你的目標名稱}>/assets"
而這個指令的意思就是建立符號連結(Symbolic Link),如果是熟悉類 Unix 系統的使用者想必很熟悉,但 Windows 使用者可能就未必了,基本上這個東西跟 Windows 上的捷徑(.lnk
)是截然不同的東西,功能雖然很像但基本上是完全不一樣,根據微軟官方的解釋,【符號連結是指向另一個檔案系統物件的檔案系統物件】,符號連結在檔案總管中會顯示為一般檔案或者是目錄,使用者或應用程式可以用完全相同的方式處理或是存取它們。
現在知道這個指令是在創建符號連結,但是路徑到底是哪裡呢?我們可以看出這邊有 CMake 的變數:${CMAKE_CURRENT_SOURCE_DIR}
和 $<TARGET_FILE_DIR:目標名稱>
,前者為你當前 CMakeLists.txt
的所在根目錄,後者則是 CMake 生產器表達式,它的語法其實是 $<TARGET_FILE_DIR:tgt>
,這其中的 tgt
就是你專案的目標名稱,比如說 add_executable(my-target)
,那 my-target
就是你的目標名稱,但如果你是有設定變數的話,例如 set(MY_EXECUTABLE "my-target")
,那你的目標名稱反而要變成 ${MY_EXECUTABLE}
。
回到主題,$<TARGET_FILE_DIR:目標名稱>
回傳的就是你程式建置好的所在目錄(可以說是建置根目錄),所以我們只要把符號連結建立在該目錄下,你的外部檔案就可以被程式讀取到了。
CMake 可以透過參數 -E
來實現例如建立符號連結 create_symlink
、複製資料夾 copy_directory
、刪除檔案 remove
、cat
和 touch
等行為。