2D 圖形
今天電視、電腦顯示屏、手機、平板電腦用的顯示圖形基本都是光柵圖形,我們常稱之爲位圖。這些屏幕由像素點構成,每個像素點代表了不同顏色。分辨率就是像素點方格的寬度和高度。例如,1920 × 1080,即1080p,意味着有1080行的像素點,每行由1920個像素點組成。類似的,3840 × 2160,即4K,意味着每行有3840個像素點,總共由2160行。
每個像素的色調最常見的就是將紅色®、綠(G)、藍(B)三種顏色混合在一起。三種顏色不同強度的組合,形成了色域。要使顯示器顯示 RGB 圖像,顯示器必須知道每個位置上像素的顏色。除了 GRB 之外,很多遊戲內部都會用 alpha 值來控制透明度。
顏色緩衝區
在計算機圖形中,顏色緩衝區是內存中包含整個屏幕顏色信息的內存區域。顯示屏可以使用顏色緩衝區在屏幕上繪製內容。將顏色緩衝區視爲二維數組,其中每個 (x, y) 索引對應於屏幕上相應位置的像素。在遊戲循環“生成輸出”階段的每個幀中,遊戲都會將圖形輸出寫入到顏色緩衝區。
顏色緩衝區的內存使用率取決於色彩深度(color depth),即儲存1像素的顏色所用的位數,它也稱爲位/像素(bpp)。舉個例子,一個24比特的顏色深度,紅、綠、藍每個使用8個bit。意味着有 或者說 16,777,216 種獨一無二的顏色。如果遊戲還要另外使用8位來存儲 alpha 值,每個像素總共需要32位來存儲。
一個1080p(1920 × 1080)的圖像,每個像素點32位,那麼大概需要的內存空間就是1920 × 1080 × 4 bytes,大概是 7.9 MB。一些現代遊戲使用16位來表示 RGB 的每個組成,這可以增加顏色的數量。當然,這將導致內存的使用量是原來的兩倍,一張1080p的圖像就接近16MB。不過現在顯卡的顯存基本都有幾千MB,這個使用量還是顯得微不足道。
色彩的表示
要在代碼中表示顏色,通常有兩種方法。假如給定一個8bit的顏色值,一種方法是簡單的使用非負整數值去代表每個顏色(通道,channel)。8bit色彩深度的通道,值就介於0到255之間。另外一種方法就是將這個值規範化到0.0到1.0之間。
使用浮點數的一個好處是可以不用過分地關注色彩深度。舉個例子,RGB 顏色(1.0, 0.0, 0.0)是一個純粹的紅色。在8bit色彩深度下表示成非負整數是(255, 0, 0),但是如果是在16bit的色彩深度下,它不再是純紅色,而接近黑色。
在這兩種表示之間轉換是很簡單的。給定一個非負整數值,除以非負整數的最大值就可以獲得規範化的浮點數。給定一個顏色的浮點數表示,乘以非負整數的最大值,就可以獲得非負整數的表示形式。
SDL 接受的是非負整數的表示形式
雙緩衝
屏幕刷新的頻率可能不同於遊戲刷新的頻率。有的顯示屏刷新的頻率是 59.94 Hz,這就意味着它比每秒60次的刷新頻率要低。有的顯示屏刷新頻率支持 144Hz 的刷新頻率,這比遊戲的刷新頻率的2倍還多。
此外, 目前的任何顯示技術都不能立即更新整個屏幕。總是有一些更新順序——是逐行、逐列等等。假設遊戲寫入顏色緩衝區,同時從相同的顏色緩衝區中讀取顏色的來顯示。由於遊戲幀速率的計時可能與顯示器的刷新率不匹配,因此有可能還在遊戲正在寫入緩衝區時同時在讀取顏色緩衝區。這是有問題的。
很容易想到的一個問題就是遊戲正在寫入 b 幀,用來覆蓋顏色緩衝區中的 a 幀。然而,還沒等 b 幀寫入結束,該畫面就被讀取,造成只顯示部分的 a 幀和部分的 b 幀。這種現象稱爲畫面撕裂(screen tearing)。
要解決畫面撕裂的問題,可以採用雙緩衝區。將遊戲寫入的顏色緩衝區和屏幕讀取的顏色緩衝區分開,交替的讀取和寫入這兩個緩衝區。一般遊戲寫入的緩衝區叫後緩衝區(back buffer),而屏幕讀取的是前緩衝區(front buffer)。可是,雙緩衝區本身並不能解決畫面撕裂的問題。如果要顯示 x 緩衝區的時候,遊戲要寫入該緩衝區。這種情況通常發生在遊戲更新過快的時候,一樣會導致畫面撕裂。
解決這個問題的方法是同步,交換緩衝區的時候必須等到顯示完成。換句話說,遊戲必須等到屏幕顯示完成才能開始寫入。這被稱爲垂直同步(vertical synchronization, vsync),由顯示屏發送刷新的信號。
在垂直同步的情況下,有可能遊戲刷新需要等待更長的時間。這就意味着遊戲循環可能達不到 30 或 60 FPS。這可能是有的玩家無法接受的幀速。因此,是否啓用垂直同步不同遊戲、不同用戶的選擇可能不同。一個好主意是在引擎中提供 vsync 作爲一個選項,這樣就可以在偶爾的屏幕撕裂或偶爾屏幕卡殼之間做出選擇。
現在,一些高端的顯示屏採用了新的顯示技術,可以做到自適應刷新頻率(adaptive refresh rate),保持和遊戲的刷新率一樣以避免同步問題。當然,這種屏幕很貴。iPad Pro 現在就支持這種顯示技術,根據瀏覽的內容自動調整刷新率,既可以保證流暢的體驗,又節約能源。
實現基本的 2D 圖形
SDL 有一組簡單的函數可以實現 2D 圖形的繪製。爲了使用 SDL 的圖形代碼,需要通過 SDL_CreateRenderer
來構造 SDL_Renderer
。渲染器(renderer)支持繪製 2D 和 3D。在這之前,先在 Game
類中添加 mRenderer
作爲成員變量,同時在構造函數中增加 nullptr
的默認初始化:
// 渲染器
SDL_Renderer* mRenderer;
在 Game.cpp
中:
Game::Game()
:mWindow(nullptr)
,mRenderer(nullptr)
,mIsRunning(true)
{
}
下一步,在初始化 Game::Initialize()
時,同時初始化渲染器。
// 創建渲染器
mRenderer = SDL_CreateRenderer(
mWindow,
-1,
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC
);
if (!mRenderer)
{
SDL_Log("創建渲染器失敗: %s", SDL_GetError());
return false;
}
第一個參數是顯示的窗口。第二的參數是用來指定驅動程序的索引,如果遊戲有多個窗口,這可能是需要考慮的。不然的話採用 -1 由 SDL 來決定。SDL_RENDERER_ACCELERATED
是採用硬件加速,SDL_RENDERER_PRESENTVSYNC
就是用來保持刷新率一致的。
還有另外兩個標誌:
SDL_RENDERER_SOFTWARE
、SDL_RENDERER_TARGETTEXTURE
,見 API 文檔
檢驗創建是否成功的手法和前面創建窗口時是一樣的。最後,我們需要在離開的時候手動銷燬渲染器。通常析構(銷燬)順序和構造順序相反,因此先銷燬渲染器,再銷燬窗口。
void Game::Shutdown()
{
SDL_DestroyRenderer(mRenderer);
SDL_DestroyWindow(mWindow);
SDL_Quit();
}
渲染 Tiffany 藍
有一種藍,代表着一種浪漫與幸福。有一種藍,它每的一個細節和理念,都始終只詮釋兩種東西,這兩種東西,一種叫愛,而另一種叫美。這種藍,就叫做Tiffany(蒂芙尼)藍。
任何遊戲圖形庫繪製圖形時都包含這三個步驟:
- 清空後緩衝區的顏色(當前遊戲的緩衝區)
- 寫入整個遊戲的場景
- 交換前後緩衝區
渲染圖形屬於最終的輸出,我們把這部分代碼放到 Game::GenerateOutput
之中。要清除後緩衝區,需要用 SDL_SetRenderDrawColor
先指定一種顏色。這個函數接受一個渲染器的指針,和RGB色值(0到255)外帶一個 Alpha 透明度。Tiffany 藍的 RGB 色值是(129, 216, 209)。
void Game::GenerateOutput()
{
// 設置 Tiffany 藍
SDL_SetRenderDrawColor(
mRenderer,
129, // R
216, // G
209, // B
255 // A
);
}
接着,調用 SDL_RenderClear
清理後緩衝區,最終用 SDL_RenderPresent
交換前後緩衝區:
// 清理後緩衝區
SDL_RenderClear(mRenderer);
// 交換前後緩衝區
SDL_RenderPresent(mRenderer);
最終
現在,編譯運行項目,就可以看到這浪漫幸福的蒂芙尼藍了: