# 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}]"}
Expand menu