# SDL2: 視窗與圖片
###### tags: `SDL`
## 圖片是如何弄到螢幕上去的?
關於圖片,SDL藉由幾個不同的**物件**達成:
:::warning
⚠️ 前面說過,SDL透過struct+pointer實現類似C++的物件(object)甚至繼承(inheritance)等特性,因此本文以下將用**物件**概括指稱這種實現方式。
:::
1. SDL_Window: 負責**視窗。**程式打開的視窗大小、邊界、行為都由它決定,但是只有SDL_Window的話只會顯示黑色的空視窗,其餘的不會顯示。
2. SDL_Surface 和 SDL_Texture: 負責**儲存圖像**,但只是將圖片弄進其中之一儲存並不會讓它顯示。必須要將這些Surface/Texture “黏” 到Window上,圖片方能顯示。
:::warning
⚠️ 以下的教學我大部分只教SDL_Texture的實現方式,原因包括但不限於: SDL_Texture輕鬆支援透明度、SDL_Texture是硬體加速(快)、SDL_Surface的邏輯真的太怪了。
:::
1. SDL_Renderer: 顧名思義是**渲染器。**功能是將Texture上的資料弄到Window上,Surface則有另外的實現方法。**Renderer是與Window連結的,這代表:**
1. 一個renderer操作一個window,若你的遊戲有兩個window,就需要兩個Renderer。
2. 不能同時創建多個renderer給同一個window,這表示你的城市若有很多file,renderer需要是全域(program-scope)的。關於program-scope和file-scope的差別,見參考資料。
若用比喻的方式,Window像是空白的布告欄,Texture像是海報,Renderer像是釘槍,三者共同運作就能將圖片傳達出去。
## SDL_Window: 視窗之創造
記住,SDL裡面都是使用物件的pointer在處理物件,舉凡函數的呼叫、物件的創造與消滅,都是引入/回傳物件指標。所以想要使用SDL_Window,我們首先創建一個SDL_Window指標:
```cpp
SDL_Window* WINDOW = NULL;
/*
型別: SDL_Window*
名稱: WINDOW
初始值: NULL(0x000000);
*/
```
### SDL_CreateWindow
使用SDL_CreateWindow這個函數創造視窗:
```cpp
//Syntax
WINDOW = SDL_CreateWindow(const char *title, int x, int y, int w, int h, Uint32 flags);
//Example
WINDOW = SDL_CreateWindow( "HERE is Your Program's Name", SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED, 640, 480, SDL_WINDOW_SHOWN );
```
各參數解釋如下:
| 名稱 | 功能 |
|:----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| title | 視窗之標題。 |
| x, y | 視窗左上角之位置,可用兩種旗標:<br>SDL_WINDOWPOS_UNDEFINED — 隨便生成<br>SDL_WINDOWPOS_CENTERED — 生成於螢幕正中間 |
| w, h | 視窗之寬、高 |
| flags | 其他選項:<br>SDL_WINDOW_SHOWN — 正常生成可視窗口<br>SDL_WINDOW_FULLSCREEN_DESKTOP — 以電腦解析度生成全螢幕<br>SDL_WINDOW_HIDDEN — 視窗不可見 <br>SDL_WINDOW_BORDERLESS — 沒有一般程式視窗的介面(如: Icon、標題、關閉視窗) <br>不衝突的Flags可以把他們OR起來一起使用! |
### SDL_DestroyWindow
```cpp
//Syntax
SDL_DestroyWindow(SDL_Window * window);
//Example
SDL_DestroyWindow(WINDOW);
```
把欲消滅的SDL_Window指標傳入即可。
### SDL_HideWindow / SDL_ShowWindow
```cpp
//Syntax
SDL_HideWindow(SDL_Window * window);
SDL_ShowWindow(SDL_Window * window);
```
把欲隱藏、顯示的SDL_Window指標傳入即可。可以用在暫時關閉的子視窗中。**主程式結束時請使用Destroy而非Hide!**
## SDL_Renderer: 視窗的渲染器
我們同樣從創建空的物件指標開始:
```cpp
SDL_Renderer* REND = NULL;
```
### SDL_CreateRenderer
```cpp
//Syntax
REND = SDL_CreateRenderer(SDL_Window * window, int index, Uint32 flags);
//Example
REND = SDL_CreateRenderer(WINDOW, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
```
各參數解釋如下:
| 名稱 | 功能 |
| --- | --- |
| window | 傳入視窗的指標。 |
| index | 選取要渲染的硬體裝置,請直接打-1不要浪費時間研究。 |
| flags | 比較常見的兩個flags是: <br>SDL_RENDERER_ACCELERATED — 硬體加速<br>SDL_RENDERER_PRESENTVSYNC — 支援垂直同步(Vertical Sync),即是使遊戲的視窗更新與螢幕更新率同步,避免螢幕撕裂(Screen Tearing)。垂直同步之後會再解釋,總之多加無害啦!|
### SDL_DestroyRenderer
```cpp
//Syntax
SDL_DestroyRenderer(SDL_Renderer * renderer);
```
把欲消滅的SDL_Renderer指標傳入即可。
---
## Checkpoint!
到目前為止,語法都還很簡單。如果你一直有跟上腳步,你的程式應該長的像這樣:
```cpp
#include "SDL.h"
#include "SDL_image.h"
using namespace std;
SDL_Window* WINDOW;
SDL_Renderer* REND;
void InitializeSDL(){
...
}
int main(int argc, char *argv[]){
InitializeSDL();
WINDOW = SDL_CreateWindow( "HERE is Your Program's Name", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, SDL_WINDOW_SHOWN );
//You'd better check if it success!
if(WINDOW == NULL){
printf("Window could not be created! SDL_Error: %s\n", SDL_GetError() );
}else{
REND = SDL_CreateRenderer(WINDOW, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if(REND == NULL){
printf("Renderer could not be created! SDL_Error: %s\n", SDL_GetError() );
}else{
//code here
}
SDL_DestroyRenderer(REND);
}
SDL_DestroyWindow(WINDOW);
return 0;
}
```
請確定你知道前面每一行的功能再繼續往下喔!
## LTexture: LazyFoo的傑作、人類的救星
在實際介紹原生的方法之前,我先借用一下LazyFoo寫好的Wrapper class: LTexture。利用這個Class可以解決很多複雜的實作,我們只需要呼叫幾個函數就可以完成所有需要的事情,原理什麼的先放一邊去! Class定義如下:
```cpp
class LTexture
{
public:
//Initializes variables
LTexture();
//Deallocates memory
~LTexture();
//Deallocates texture
void free();
//Loads image at the specified path
bool loadFromFile( std::string path , SDL_Renderer*);
//Set color modulation
void setColor( Uint8 red, Uint8 green, Uint8 blue );
//Set blending
void setBlendMode( SDL_BlendMode blending );
//Set alpha modulation
void setAlpha( Uint8 alpha );
//Renders texture at the given point
void render(float x, float y, SDL_Renderer*);
void render(float x, float y, float w, float h, SDL_Renderer*);
void render(SDL_FRect& dstrect, SDL_FRect& srcrect, SDL_Renderer*);
//Explained in Unit 5.
//Load Texture from text
bool loadFromRenderedText( std::string text, SDL_Color, SDL_Renderer*, TTF_Font*);
//Render center-aligned text
void renderCenter(float x, float y, SDL_Renderer*);
void renderRight(float x, float y, SDL_Renderer*);
//Gets image dimensions
float getWidth();
float getHeight();
//set mHeight and mWidth
void setdim(float w, float h);
protected:
//The actual hardware texture
SDL_Texture* mTexture;
//Image dimensions
float mWidth;
float mHeight;
};
```
我們可以先從變數開始看起,總共有三個變數,第一個是SDL_Texture*,是這Class的主體,後續兩個變數儲存這個Texture的完整大小。接下來看個個函數的實際程式碼和功能。
### 建構子(Constructor)
```cpp
LTexture::LTexture()
{
//Initialize
mTexture = NULL;
mWidth = 0;
mHeight = 0;
}
```
因為很簡單就不介紹了!
### 解構子(Destructor) 和 free()
```cpp
void LTexture::free()
{
//Free texture if it exists
if( mTexture != NULL )
{
SDL_DestroyTexture( mTexture );
mTexture = NULL;
mWidth = 0;
mHeight = 0;
}
}
LTexture::~LTexture(){
free();
}
```
free()呼叫了一個SDL_DestroyTexture函數解決SDL_Texture的部分,其餘變數設為NULL、0。而解構子是直接呼叫free(),**差別在於free()可以手動呼叫,而程式結束時會自動呼叫解構子。**
### loadFromFile(): 載入
```cpp
bool LTexture::loadFromFile( std::string path, SDL_Renderer* renderer)
{
//Get rid of preexisting texture
free();
//The final texture
SDL_Texture* newTexture = NULL;
//Load image at the specified path
SDL_Surface* loadedSurface = IMG_Load( path.c_str() );
if( loadedSurface == NULL )
{
printf( "Unable to load image %s! SDL_image Error: %s\n", path.c_str(), IMG_GetError() );
}
else
{
/*
//Color key image: Optional
//Further explanation in Unit ?
SDL_SetColorKey( loadedSurface, SDL_TRUE, SDL_MapRGB( loadedSurface->format, 0, 0xFF, 0xFF ) );
*/
//Create texture from surface pixels
newTexture = SDL_CreateTextureFromSurface( renderer, loadedSurface );
if( newTexture == NULL )
{
printf( "Unable to create texture from %s! SDL Error: %s\n", path.c_str(), SDL_GetError() );
}
else
{
//Get image dimensions
mWidth = loadedSurface->w;
mHeight = loadedSurface->h;
}
//Get rid of old loaded surface
SDL_FreeSurface( loadedSurface );
}
//Return success
mTexture = newTexture;
//Optional
this->setBlendMode( SDL_BLENDMODE_BLEND );
return mTexture != NULL;
}
```
看的出來背後的原理非常複雜,連我也不是很想理解,所以我們快轉到使用的部分。這個函數幫助你載入各種格式的圖片,包括.jpg / .png / .webp等幾種比較常見的圖片格式,**我推薦使用.png,因為透明的部分載入後會保持透明,藉此省下以程式碼執行去背的麻煩。** 用法範例:
```cpp
LTexture tex;
tex.loadFromFile("img/ExampleImage.png",REND);
```
路徑的部分建議採相對路徑,是**與exe檔的相對路徑**。
### render(): 渲染
render的部分我改進了LazyFoo的程式,加入了SDL3(根據Wiki標示)的新東西,不過在SDL2已經有支援了! 那就是可以以浮點數進行渲染的大小、長寬調整,大幅增加遊戲的精度,尤其是動畫量很大的遊戲。**以下函數分成三種,一是指定座標,二是指定座標和大小,三是完全控制。**
```cpp
void LTexture::render( float x, float y, SDL_Renderer* renderer){
//Set rendering space and render to screen
SDL_FRect target = { x, y, mWidth, mHeight };
//Render to screen
SDL_RenderCopyF( renderer, mTexture, NULL, &target);
}
void LTexture::render( float x, float y, float w, float h, SDL_Renderer* renderer){
//Set rendering space and render to screen
SDL_FRect target = { x, y, w, h };
//Render to screen
SDL_RenderCopyF( renderer, mTexture, NULL, &target);
}
void LTexture::render( SDL_FRect& dstrect, SDL_FRect& srcrect, SDL_Renderer* renderer){
//Render to screen
SDL_RenderCopyF( renderer, mTexture, srcrect, dstrect);
}
```
以下分別介紹三種使用上的差別:
- Type 1: 將原Texture的**全部**,以**原比例**渲染到螢幕上的指定座標(左上角座標)。
- Type 2: 將原Texture的**全部**,以**指定的大小(w,h)** 渲染到螢幕上的指定座標(x,y,左上角座標),**比例與原比例不同時自動縮放成指定大小,長寬比不同時自動拉伸壓縮**。
- Type 3: 將原Texture的**指定區域**(以srcrect長方形指定),以**指定的大小**(以dstrect長方形指定)渲染到螢幕上的指定座標(左上角座標),**比例與原比例不同時自動縮放成指定大小,長寬比不同時自動拉伸壓縮**。
SDL_FRect是struct,直接以大括弧初始化即可,亦可以x,y,w,h更改指定的參數。例如:
```cpp
SDL_FRect rect = {30, 10, 20, 40}; //{x, y, w, h}
rect.w = 100; //rect: {30, 10, 100, 40}
```
最後提醒一下**渲染的順序會影響呈現的結果,先渲染的物件會在較下方的圖層。** 對於圖層沒有概念的人,可以想像你在貼海報,後貼上去的海報會遮擋掉前面貼上的海報,若後貼的海報是半透明的,會變成兩個海報都可以看到一點點。所以呈現時必須想好呼叫render的先後順序。再者,**同一個texture可以重複渲染到不同位置,但是圖層的先後順序互不影響。**
### setAlpha(): 設定透明度
```cpp
void LTexture::setAlpha( Uint8 alpha )
{
//Modulate texture alpha
SDL_SetTextureAlphaMod( mTexture, alpha );
}
```
請以0\~255設定透明度。0是完全不透明,255是完全透明。亦可以0x00\~0xFF(16進制)設定。**預設值是0,可不必額外呼叫。**
### setBlendMode(): 設定渲染模式
```cpp
void LTexture::setBlendMode( SDL_BlendMode blending )
{
//Set blending function
SDL_SetTextureBlendMode( mTexture, blending );
}
```
使用Flag: SDL_BLENDMODE_BLEND可以讓Renderer變成一般修圖軟體的混合模式,即有透明度的混合。在loadFromFile最後我已經將它直接寫進去了,也可以不用再呼叫。其他亂七八糟的混和模式請見: [https://wiki.libsdl.org/SDL2/SDL_BlendMode](https://wiki.libsdl.org/SDL2/SDL_BlendMode)。
---
## 展示渲染結果
當所有物件都被呼叫過[object].render()之後,渲染器儲存了這些資料,最後只要請渲染器將這些物件貼到視窗上即可:
```cpp
SDL_RenderPresent(REND);
```
---
在這個章節我們探討創造視窗,以及利用LTexture將圖片渲染至屏幕上。下一個章節我們將繼續利用這個物件,達成文字的渲染。
{"metaMigratedAt":"2023-06-18T04:37:34.979Z","metaMigratedFrom":"Content","title":"SDL2: 視窗與圖片","breaks":true,"description":"關於圖片,SDL藉由幾個不同的物件達成:","contributors":"[{\"id\":\"dc908d61-271c-4fcc-a980-92fa8cb175eb\",\"add\":10426,\"del\":23}]"}