# OpenGL 材質讀取外部圖片教學
## 圖片讀取函式庫
如果想要特別讀取 BMP、JPEG 或 PNG 檔案,通常都會需要撰寫 Reder,但除了 BMP 之外其他的格式讀取或寫入都較為複雜,所以說一般建議使用已經開發好的函式庫即可。
### stb_image
#### 安裝方法 1: 直接下載 (推薦方法)
1. 首先下載 `stb_image.h` 到專案根目錄,[Github 載點](https://github.com/nothings/stb/blob/master/stb_image.h)。
2. 之後創建一個新的 `.cpp` 檔案,名稱隨意,裡面內容如下:
```c++=
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
```
3. 在要使用的檔案中(例如`main.cpp`)引入標頭檔,即可開始使用 stb 圖片函式庫了!
```c++=
#include "stb_image.h"
```
#### 安裝方法 2: 透過 vcpkg
1. 首先輸入指令來安裝 `stb` 函式庫。
```bash=
$ vcpkg install stb --triplet=x64-windows
```
2. 安裝好後,只需要在專案根目錄下的 `CMakeLists.txt` 加上相關設定
```cmake=
find_path(STB_INCLUDE_DIRS "stb.h")
target_include_directories(目標名稱 PRIVATE ${STB_INCLUDE_DIRS})
```
3. 之後創建一個新的 `.cpp` 檔案,名稱隨意,裡面內容如下:
```c++=
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
```
3. 在要使用的檔案中(例如`main.cpp`)引入標頭檔,即可開始使用 stb 圖片函式庫了!
```c++=
#include <stb_image.h>
```
#### 使用方法
讀取圖片的方式,就是這麼如此簡單:
```c++
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)` 會是左上角(想想陣列的概念),所以讀取出來的圖片會是上下顛倒的,這個時候可以透過下列函式來矯正它:
```c++=
stbi_set_flip_vertically_on_load(true);
```
> 記得要在呼叫 `stbi_load()` 之前。
### SDL Image
除了使用 `stb_image.h` 之外,還可以使用 SDL2 自己的衍生函式庫 `SDL2-Image` 來實現讀取圖片的功能。
#### 安裝方法
1. 首先輸入指令來安裝 `sdl2-image` 函式庫。
```bash=
$ vcpkg install sdl2-image[core,libjpeg-turbo] --triplet=x64-windows
```
2. 安裝好後,記得要去專案根目錄下的 `CMakeLists.txt` 做相關設定:
```cmake=
find_package(sdl2-image CONFIG REQUIRED)
target_link_libraries(目標名稱 PRIVATE SDL2::SDL2_image)
```
3. 接著引入標頭檔並且初始化,這樣就可以開始使用 `SDL2-Image` 了。
```c++=
#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;
}
// 其他程式 ....
}
```
##### 使用方法
讀取圖片的方式,就是這麼如此簡單:
```c++=
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 顛倒過來(雖然是最簡單但是不建議)。
```c++
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);
```
## 利用 CMake 建立 Symbolic Link
當你有開始有讀取外部圖片的需求時,就會發現圖片路徑是一個很大的問題,比如說你在主程式寫說要讀取 `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 來自動幫我們處理,只要輸入以下語法即可:
```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
)
```
### 指令解釋
```cmake
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 官方文件建議使用。
而上述指令大致上的意思就是說,在程式建置好後會輸入以下指令:
```bash=
$ 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 生產器表達式](https://cmake.org/cmake/help/v3.0/manual/cmake-generator-expressions.7.html),它的語法其實是 `$<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` 等行為。
### 錯誤排除
1. 如果你會發生以下錯誤的話:

其解決方案很簡單,就是到【設定】->【更新與安全性】->【開發人員專用】,然後將【開發人員模式】給它開啟起來就解決了:

2. 如果你會發生以下錯誤的話:

其解決方案很簡單,把整個建置資料夾刪除,並且也清除掉 CMake 快取,重新建立就可以解決了。
###### tags: `OpenGL`