奇怪有問題 之前沒注意到
這個問題是Count == 0
GlobalConstants_Renderer_UsedDebugCamer 打開的話會有Bug
Count是零的話就不讓排序了
game.h: 查閱 TODO 列表
大家好,歡迎來到 game Hero,這是一檔我們在直播中一起編寫完整游戲的節目。不幸的是,我們昨天完成了之前的工作。完成任務固然好,但這也意味著今天我們并不清楚接下來應該做什么。我們并不知道接下來最合適的任務是什么。因此,我現在需要查看一下任務清單,看看我們今天有什么事情可以做。我們有很多選擇,做的事情不在少數,雖然不能說我們已經完成了所有的工作,但確實有很多事情還可以繼續做。
我們已經做了一些事情,包括去掉了偶數掃描線標記法,并且添加了排序功能。這個排序功能因為某些原因似乎一直沒有在代碼中完成,因此現在的問題是我們該如何繼續推進。這里是我們可以做的所有任務的清單,這個清單是我們想到的一些需要做的事情,雖然它不完全,但它展示了我們想到的一些點:我們想做這個,想做那個。因此,接下來我們可以選擇去做的一些事情。比如說我們可以回去完善調試代碼,因為我確實想在某個時間點把它做完。還有一些小任務,比如調試音頻相關的代碼、處理線程等。
其實我自己也不確定接下來要做什么,所以我在想,也許是時候讓整個項目朝著更接近發布的狀態發展了,至少是在一些清理工作上。因為實際上距離我們制作一個完整游戲已經不遠了,很多任務基本上都是圍繞游戲本身的內容,更多的是游戲的開發和內容,而不再是底層的基礎架構。因此,我有一個疑問,也許現在是時候將任務聚焦到 Win32 平臺層,加入 OpenGL 支持。這樣一來,我們就能有一個合適的合成層,通過 Windows 來獲得垂直同步(vsync),從而確保我們在 60 幀或 30 幀的固定幀率下運行游戲,而不用再擔心計時精度等問題。這樣,我們就能在制作游戲時不再被非固定幀率的問題困擾。
win32_game.cpp: 將硬件加速提到 TODO 列表的頂部并開始實施
我們今天繼續進行排序的相關工作,雖然基本已經完成了,但還是想收尾一下,讓整個流程更完整。其實這次排序只是個借口,主要是想順便聊一聊一些計算機科學的知識點,這些內容在我們項目中平時不會特別講,更多是適合做一些自學延伸的內容。
回顧上周五的進度,當時我們嘗試實現就地歸并排序(merge sort in-place),結果發現這個做法并不可行。原因是操作成本太高,為了實現就地排序,我們必須執行大量的數據移動,而這些操作雖然邏輯上不復雜,但代價很大。
因此我們最終放棄了就地排序的嘗試,轉而采用了非就地的方式來實現歸并排序。這個版本非常容易實現,我們在短短幾分鐘內就完成了代碼編寫,而且運行效果良好。
目前這部分排序已經可以正常工作,接下來就可以繼續在已有基礎上推進后續的開發了。
運行游戲并注意到奇怪的淡出效果
現在我們有一個有點奇怪的淡入淡出效果,這個機制是后來加上的,當時是出于一個人的想法,想讓畫面從 Windows 桌面上淡入淡出,我們也沒特別的理由,就是覺得可以做一下,于是就實現了這個效果。
現在的邏輯是,畫面可以實現雙向的淡入淡出,比如啟動時從黑屏淡入,退出時再淡出回到桌面。這個效果其實不是直接在主窗口上做的,我們實際上是創建了一個專門用于淡入淡出的窗口,它的唯一作用就是把整個屏幕蓋住,用黑色進行淡入淡出處理。
而真正用來渲染游戲內容的窗口是另一個,我們創建這個窗口時,讓它的尺寸和整個屏幕一樣大,也就是說它占滿了全屏,但一開始是不可見的。等準備好后,我們再把這個窗口設置為可見狀態,然后通過 BitBlt(圖像塊傳輸)等方式將畫面繪制到這個窗口中,從而實現游戲畫面的顯示。
所以,如果我們之后還需要對這個顯示流程做調整,比如修改啟動流程或添加更多的渲染邏輯,就需要注意我們現在是用了兩個窗口:一個用于處理淡入淡出的視覺效果,另一個是實際負責顯示游戲內容的主窗口。這個設計使得我們可以把視覺過渡和實際游戲邏輯解耦,方便管理和優化。
為 OpenGL 設置環境
我們接下來無論是使用 OpenGL 還是 Direct3D,都需要做一些額外的準備工作,才能讓 Windows 系統知道我們將要使用 3D 圖形硬件進行渲染。
第一步其實相對簡單,就是在項目中鏈接合適的系統庫。之前我們已經做過類似的事情,比如列出我們所依賴的 Windows 系統服務。需要明確一點,這些庫并不是我們平常所理解的那種“包含實際函數實現的庫”,而是“導入庫(import libraries)”。這些導入庫的作用是幫助我們連接到操作系統提供的功能接口上。
舉例來說:
user32.lib
里面包含的是窗口相關的服務,例如CreateWindow
這樣的函數;gdi32.lib
提供圖形界面相關的一些基礎功能,比如GetDeviceCaps
等;winmm.lib
則是我們用來設置定時器分辨率的,這個用于調整窗口調度系統的精度。
到目前為止,我們的程序中只用了這些庫,也僅僅調用了一些基礎的窗口和定時器相關功能。
但接下來我們要和 3D 圖形硬件打交道,而這些功能并不包含在前面這三個庫中。因為我們運行在 Windows 平臺上,不可能直接越過操作系統去訪問顯卡硬件,必須通過系統提供的接口來操作。
所以,接下來的任務就是:
- 找到系統中用于訪問 3D 圖形硬件的接口;
- 鏈接這些新的導入庫;
- 正確地初始化圖形上下文,讓 Windows 知道我們將會使用硬件加速。
這一步是使用硬件加速渲染的基礎,無論選擇 OpenGL 還是 Direct3D,走的路線都類似。我們不直接控制硬件,只能通過操作系統提供的通道進行訪問和控制。后續的工作就是在這個基礎上一步步搭建起圖形渲染管線。
鏈接 opengl32.lib
我們接下來需要添加一個新的導入庫,以便能夠訪問系統的 3D 圖形接口。在 Windows 上,我們有兩個主要選擇:DirectX 或 OpenGL。
這里我們選擇添加 OpenGL,而不是 DirectX,原因是 OpenGL 具有更強的跨平臺能力。雖然我們在 Windows 上學習如何初始化 OpenGL,這一部分代碼本身不會直接在其他平臺使用,但一旦我們通過 OpenGL 實現了渲染邏輯,這部分渲染邏輯就可以通用于其他平臺,比如 macOS 和 Linux。對于教學來說,選擇更具通用性的 OpenGL 顯然更合適,它能讓大家在不同的操作系統下都受益。
根據命名習慣,我們添加的庫名是 opengl32.lib
。這個庫就是用來鏈接并調用 OpenGL 提供的系統接口,允許我們訪問并使用 3D 圖形硬件。
在添加了這個庫之后,編譯程序會成功,鏈接器不會報錯,但此時還不會有什么明顯變化,因為我們還沒有真正調用任何 OpenGL 的功能。所以運行游戲后,表現跟之前完全一樣,什么都沒有變。
換句話說,此時唯一發生的變化就是在構建系統中額外鏈接了 opengl32.lib
,為后續使用 OpenGL 做好準備。之后我們會開始編寫代碼,使用 OpenGL 初始化渲染上下文,開始進行圖形渲染。
添加庫沒報錯說明找到了
win32_game.cpp: #include <opengl32.h>
現在我們已經具備了調用操作系統中 OpenGL 相關函數的能力,就像之前為了使用 DirectSound、XInput 等系統服務所做的一樣,我們也需要包含一個 OpenGL 的頭文件。在這里,我們引入的是 gl.h
,這個頭文件中包含了我們可能需要用到的所有 OpenGL 函數聲明。
加入這個頭文件之后,我們就可以像調用其他平臺 API 一樣調用 OpenGL 的函數了。就像我們使用 windows.h
來調用 Windows API,使用 xinput.h
來調用 XInput 接口一樣。
不過,值得注意的是,對于 XInput 和 DirectSound,我們采用了“延遲綁定”(late binding)的方式,也就是在運行時通過 GetProcAddress
來獲取函數地址,而不是在鏈接階段就綁定到這些庫。這么做的原因是,這些庫在某些用戶機器上可能根本不存在,如果我們在編譯時就將它們綁定到可執行文件中,一旦用戶系統沒有這些庫,程序就會在啟動時崩潰,這是我們不希望看到的。
相比之下,OpenGL 則不需要這么謹慎。OpenGL 從 Windows NT 3.51 開始就隨系統一起提供,也就是說在所有現代 Windows 系統上,都一定會存在一個基本版本的 OpenGL。我們當前所使用的 Windows 屬于 NT 分支(而不是 Windows 95/98/ME 那種老舊的分支),所以我們可以放心地直接鏈接 opengl32.lib
并使用它提供的基本功能。
當然,這并不意味著所有現代 OpenGL 的功能都可以隨意使用。OpenGL 的發展歷程中出現了很多版本和擴展,從 1.0 到 4.x,每個版本新增了許多新特性。這些高級特性是否可用,取決于顯卡驅動的更新情況和顯卡本身的支持程度。
也就是說,雖然基本的 OpenGL 1.x 函數是可以直接使用的,但如果想使用更高級的 OpenGL 功能(比如 OpenGL 3.0 以后的東西),我們仍然需要像調用 XInput 那樣,通過動態加載的方式獲取函數指針,這就需要我們寫類似 wglGetProcAddress
這樣的加載代碼。
目前,我們只需要最基礎的 OpenGL 功能,比如把圖像繪制到屏幕上的能力,而這些基礎功能在任何支持 OpenGL 的系統中都是可以直接調用的。所以現在只需要包含 gl.h
,之后就能開始初始化和使用 OpenGL 渲染器了。后續如果我們決定更早地切換渲染器到 OpenGL,那么這些基礎知識也將派上用場。
添加gl/GL.h頭文件
#include <gl/gl.h>
#include <gl/GL.h>
是一個文件
Windows 頭文件之所以不區分大小寫,根本原因在于 Windows 文件系統的特性,以及 C/C++ 編譯器在 Windows 上的行為。下面我們詳細解釋這個問題:
一、Windows 文件系統是大小寫不敏感的
Windows 常用的文件系統(如 NTFS、FAT32)默認是:
- 大小寫不敏感(case-insensitive)
- 但它是大小寫保留的(case-preserving)
什么意思?
舉個例子:
windows 會認為 these 都是同一個文件:Windows.hwindows.hWINDOWS.H
但它會保留你最初寫入的大小寫形式,比如你創建了 Windows.h
,之后訪問 windows.h
也能找到它,但文件名會仍然顯示為 Windows.h
。
二、C/C++ 編譯器遵循操作系統行為
Windows 下的主流編譯器(如 MSVC、Clang、GCC)在處理 #include
指令時:
#include "windows.h"
#include "Windows.h"
#include "WINDOWS.H"
這些它都會成功找到,因為它最終依賴的是 Windows 文件系統的查找機制,而不是自己去實現一套區分大小寫的邏輯。
三、與 Linux/Mac 的區別
而在 Linux 或 macOS 上就不同了:
- 大多數默認的文件系統(如 ext4、APFS)是 大小寫敏感的(case-sensitive)
- 所以在這些系統上,
#include "Windows.h"
和#include "windows.h"
就可能是完全不同的文件,找不到就會報錯。
總結一句話:
Windows 的頭文件不區分大小寫,是因為 Windows 文件系統默認大小寫不敏感,編譯器順應這一行為。
如果你在寫跨平臺代碼,建議還是始終使用正確的大小寫拼寫,這樣可以避免在 Linux/macOS 上編譯失敗。需要更嚴謹的做法時,也可以在 Windows 上使用大小寫敏感的子系統(比如 WSL 的 ext4 掛載)。
win32_game.cpp: 引入 Win32InitOpenGL
我們現在需要做的是讓窗口具備使用 OpenGL 的能力。就像之前初始化 DirectSound 一樣,我們需要經歷一個初始化流程,不過這次針對的是 OpenGL。
雖然 DirectSound 需要動態加載庫(比如使用 LoadLibrary
和 GetProcAddress
),但 OpenGL 不需要這樣處理。原因在于 OpenGL 是 Windows 自帶支持的系統服務,就像 user32
那樣是一直存在的,因此我們可以直接靜態鏈接 OpenGL32.lib
并調用其中的函數,而不必擔心目標機器上缺失該功能。唯一的不確定是其支持的 OpenGL 版本,因為這取決于顯卡和驅動程序的版本,但至少最基礎的一部分(OpenGL 1.x)是一直存在的。
接下來,我們要做的事情和初始化 DirectSound 時的流程類似,我們需要經歷幾個具體的步驟。這些步驟雖然不算多,但確實比較“詭異”和不直觀,很容易出錯,因此也不好完整記住,所以我們可能需要查看一些示例代碼作為參考。畢竟,這種初始化過程不像一般的 API 調用那么常規和清晰。
為此,我們打算寫一個初始化 OpenGL 的函數,比如叫 Win32InitOpenGL
。目前為了避免打亂已有結構,我們打算把這個函數先單獨放進當前文件,避免和其他邏輯混在一起。
我們當前的目標很明確:我們已經有一個窗口,現在需要讓這個窗口附加一個可以與 OpenGL 一起工作的上下文。也就是說,我們要為這個窗口創建一個 OpenGL 上下文,使得它能支持 OpenGL 渲染。
在寫具體的代碼之前,我們要先了解 OpenGL 的模型,畢竟設置過程的詭異之處多半也是由于這個模型本身的歷史遺留設計所導致的。我們需要理清楚整個初始化流程,包括像素格式設置、設備上下文創建、渲染上下文建立與綁定等操作。
總之,現在的任務是為當前窗口建立一個 OpenGL 渲染環境,接下來將從 OpenGL 的整體模型入手,逐步實現這個目標。
Blackboard: Windows 上的 OpenGL
我們現在來了解一下 OpenGL 在 Windows 上的運作方式。
OpenGL 是一個圖形 API,最初來源于一個叫 Silicon Graphics(簡稱 SGI)的公司。在個人電腦還沒有圖形加速卡的年代,SGI 專門制造擁有硬件加速能力的高端圖形工作站。這些設備可以完成在當時看來幾乎不可思議的圖形任務,比如以硬件方式快速繪制實心三角形或進行紋理映射等。
SGI 開發了一個專門用于控制其圖形硬件的 API,最初叫 GL,后期也被稱為 Iris GL(Iris 是他們的一個產品線)。這個 API 后來被標準化成了 OpenGL,成為一個更開放的平臺標準,以便于各個平臺都可以實現并運行這個圖形接口。這樣一來,開發者編寫的圖形程序可以直接在 SGI 的設備上獲得硬件加速,從而提升運行效率。
OpenGL 最初的功能非常基礎,僅包含諸如繪制線條、填充多邊形、進行基礎紋理映射等操作,沒有現代 GPU 的高級特性,如著色器或并行計算功能。這些高級功能都是后來通過 OpenGL 的擴展機制陸續加入的。
隨著時間推移,OpenGL 逐漸演變為兩部分:
-
平臺無關部分:這部分是我們通常認為的 OpenGL,比如函數
glVertex3f
、glClear
等,它們用于描述和控制圖形渲染過程,這部分由 OpenGL 的標準委員會(如 Khronos Group)統一維護和規范。 -
平臺相關部分:這部分處理如何在具體平臺上啟動和運行 OpenGL。每個平臺由于架構和窗口系統的不同,初始化和集成 OpenGL 的方式也不同。例如在 Windows 上,我們必須處理窗口創建、像素格式設置、設備上下文與渲染上下文的綁定等。而這些流程沒有在 OpenGL 的標準中定義,完全依賴于平臺自身提供的機制。
在 Windows 上,這些平臺相關操作通過一組以 wgl
前綴開頭的 API 實現(意為 Windows OpenGL),例如:
wglCreateContext
:創建一個 OpenGL 渲染上下文;wglMakeCurrent
:將渲染上下文綁定到當前線程;wglDeleteContext
:刪除渲染上下文;wglGetProcAddress
:獲取擴展函數地址。
這些函數不屬于 OpenGL 的核心規范,而是 Windows 提供的專門用于支持 OpenGL 的機制。
但需要注意的是,并不是所有 OpenGL 的相關調用都遵循 wgl
前綴的命名。例如 SwapBuffers
就沒有前綴,它屬于 GDI(圖形設備接口)的一部分,用于在雙緩沖時交換前后臺緩沖區。
總結起來:
- OpenGL 的標準部分處理圖形渲染本身;
- 初始化和集成 OpenGL 到應用程序中,依賴平臺提供的接口(如 Windows 提供的
wgl
系列); - Windows 上運行 OpenGL 程序必須與其窗口系統深度配合,設置像素格式、創建渲染上下文等;
- OpenGL 的最初版本非常基礎,現代許多高級特性依賴于擴展機制;
- SGI 是 OpenGL 背后的起源,NVIDIA、3dfx 等后來的圖形硬件廠商很多也都源自 SGI 的工程師。
現在我們要做的,就是圍繞 Windows 的這些平臺特有部分,實現一套 OpenGL 初始化流程,為窗口提供渲染支持。
Blackboard: “DC” -> 設備上下文
在 Windows 編程中,我們之前接觸過一個叫做 設備上下文(Device Context, DC) 的概念。它是 Windows 中用于繪圖操作的一個狀態集合,用來描述當前繪圖的相關信息,比如坐標變換、當前畫筆顏色、繪圖模式等。而我們通過 HDC
(Handle to Device Context)來獲取對設備上下文的訪問權限,并進行圖形操作,比如用 FillRect
這樣的函數繪制矩形。
OpenGL 中也有一個非常類似的機制,叫做 渲染上下文(Rendering Context, RC),也就是 OpenGL 的 RC(HGLRC
表示)。這個 RC 是用來保存 OpenGL 當前狀態的對象,比如當前使用的著色器、當前綁定的紋理、清除顏色等。當我們在 OpenGL 中調用函數時,這些操作并不會直接傳入繪圖目標,而是默認作用于當前線程綁定的渲染上下文。
Windows 中 DC 與 OpenGL 中 RC 的關系
我們在 Windows 中繪制圖形,比如調用 FillRect
,需要傳入目標的 HDC
,系統就知道我們要對哪個窗口進行繪制。而在 OpenGL 中,比如調用 glClear
(用于清除顏色緩沖區),并沒有任何與目標窗口相關的參數,這是因為:
- OpenGL 的狀態是綁定到線程的。
- 我們需要手動將一個
HGLRC
(OpenGL 渲染上下文)綁定到當前線程,這樣后續的 OpenGL 命令才會知道作用于哪個窗口。
這就是 wglMakeCurrent
的作用:將某個渲染上下文與當前線程綁定。只有綁定成功之后,線程才可以正常執行 OpenGL 調用。
渲染流程的初始化目標
我們當前的目標是用 OpenGL 在窗口中做一件簡單的事 —— 清屏操作。即我們嘗試用 OpenGL 把窗口背景清成某種醒目的顏色,比如粉紅色。這一步的目的是確認 OpenGL 渲染管線在 Windows 中被正確初始化和設置了。
為了做到這一點,我們需要完成以下步驟:
- 獲取設備上下文 HDC:從窗口中取得 Windows 圖形系統的設備上下文,用于與窗口表面進行交互。
- 設置像素格式:告訴系統我們想用什么樣的像素格式(比如支持 OpenGL、顏色深度、雙緩沖等)。
- 創建 OpenGL 渲染上下文 HGLRC:基于我們設置好的像素格式和 HDC 創建一個渲染上下文。
- 綁定渲染上下文到當前線程:通過
wglMakeCurrent
將HGLRC
和HDC
綁定到當前線程中,這樣這個線程發出的 OpenGL 命令就知道該作用在哪個窗口上。 - 執行 OpenGL 命令進行繪制:比如
glClearColor
設置清除顏色,glClear
執行清屏。
這種機制與 Windows 自身的設備上下文系統是相互協作的,我們可以理解為在 Windows 的圖形子系統上層,疊加了一個 OpenGL 渲染子系統,它擁有自己的狀態管理方式并通過渲染上下文來連接系統圖形資源與 OpenGL 狀態。
這個架構是線程相關的,也就是說:
- 每個線程只能有一個活躍的 OpenGL 渲染上下文;
- 一個渲染上下文也只能在一個線程中活躍使用;
- 如果要在多個線程中使用 OpenGL,需要進行顯式的綁定和切換。
總體來看,這些概念雖然聽起來復雜,但本質上是:Windows 管系統的設備上下文,OpenGL 管圖形渲染的狀態,而我們需要在它們之間建立綁定橋梁,才能在窗口上使用 OpenGL 繪圖功能。
win32_game.cpp: 編寫 Win32InitOpenGL
在 Windows 中使用 OpenGL 時,創建和啟用一個 OpenGL 渲染上下文的流程看起來簡單,但實際上還有很多隱藏的細節。首先,我們要調用 wglCreateContext
來創建一個 OpenGL 渲染上下文(Rendering Context,簡稱 RC)。這一步需要傳入一個設備上下文(HDC),這個 HDC 通常是從一個窗口中通過 GetDC
獲取到的。
這個 RC 是 Windows 系統中對 OpenGL 渲染狀態的封裝,類似于一個句柄(handle),類型為 HGLRC
。調用 wglCreateContext
成功之后,我們就擁有了一個 OpenGL 渲染上下文。
不過,僅僅創建出來還不夠。創建一個 RC 并不意味著當前線程正在使用它。為了讓 OpenGL 知道當前線程要使用哪個渲染上下文,我們需要調用 wglMakeCurrent
,把我們創建的 RC 綁定到線程上。這個函數需要傳入兩個參數:
- 一個
HDC
(設備上下文),用于指定這個 RC 是和哪個窗口表面關聯的; - 一個
HGLRC
(渲染上下文),就是我們剛才用wglCreateContext
創建的。
HANDLE GL Render Context
一旦調用 wglMakeCurrent
成功,這個線程就可以開始使用 OpenGL 命令了,所有的 OpenGL 狀態都會隱式關聯到當前線程。
這一切聽起來似乎非常簡單,但實際上,這只是表層工作。僅靠 wglCreateContext
和 wglMakeCurrent
并不足以讓 OpenGL 在窗口中正確工作。雖然代碼可能可以編譯通過,流程也看似完整,但如果嘗試運行,很可能無法在窗口中看到任何渲染結果。
這背后的原因是 OpenGL 的使用依賴一系列更復雜的準備工作,比如:
- 必須先設置一個合適的像素格式(Pixel Format);
- 必須確保窗口支持 OpenGL 渲染;
- 必須處理雙緩沖機制、上下文版本要求、多平臺兼容性等問題。
因此,雖然我們看起來只寫了幾行代碼就完成了 RC 的創建和綁定,但實際上這遠遠不夠。之后我們還需要深入處理這些基礎設施部分,才能讓 OpenGL 正常地在窗口中進行渲染操作。
總結一下當前流程:
- 從窗口獲取 HDC;
- 調用
wglCreateContext
創建 OpenGL RC; - 使用
wglMakeCurrent
把 RC 綁定到當前線程; - 此時理論上就可以調用 OpenGL 的函數了。
但這僅是啟動流程的一部分,后面還會逐步處理像素格式、渲染目標配置等復雜問題,才能真正讓 OpenGL 正常工作。表面簡單,實則隱含了許多平臺依賴與細節處理。
win32_game.cpp: 禁用 Win32DisplayBufferInWindow
我們現在要做的是,先暫時移除原本將離屏緩沖區內容顯示到屏幕上的流程,具體就是把現有的那段使用 StretchDIBits
顯示畫面的代碼暫時禁用掉。這段代碼原本是將 CPU 渲染好的圖像拷貝到窗口上,也就是所謂的軟件渲染方式。
禁用掉這些顯示邏輯之后,當運行程序時,整個游戲畫面就會消失,不再顯示我們原本渲染好的內容。屏幕上可能會出現一片黑,或者是一些殘留數據、未定義內容,總之看不到之前的游戲界面了。
這是因為,我們現在不再使用那套軟件方式來把畫面刷到窗口,而是準備轉向使用 OpenGL 來處理圖像的呈現。也就是說,我們開始切換渲染管線,不再從 CPU 拷貝圖像到窗口,而是通過 GPU 的 OpenGL 來直接控制顯示過程。
這一步是為接下來的 OpenGL 初始化和測試作準備——只有當原來的顯示方式被移除,才能確認 OpenGL 是否能正確接管圖像輸出。所以這一步操作的意義就是清空舊的顯示路徑,讓我們能夠干凈地測試新建立的 OpenGL 渲染流程是否能夠成功把圖像輸出到窗口。
win32_game.cpp: 在該函數中插入一些 OpenGL 代碼
我們現在要在現有的程序中插入一些基礎的 OpenGL 渲染操作,這樣就可以測試 OpenGL 是否真的已經正確初始化并且可以工作了。整個過程其實非常簡單,主要分為三個步驟:
第一步:設置清屏顏色(glClearColor)
我們調用 glClearColor
來設置清屏時要使用的顏色。這個函數接受四個浮點數參數,分別表示紅、綠、藍、以及 alpha(透明度)分量。例如,如果我們傳入 (1.0f, 0.0f, 1.0f, 0.0f)
,那就是一個非常鮮艷的紫色,alpha 為 0。
雖然我們設置的是浮點數顏色,但最終 OpenGL 會自動將這些浮點顏色轉換成幀緩沖區支持的格式,比如 8 位色深等,所以不用擔心底層的存儲格式問題。
第二步:清除顏色緩沖區(glClear)
設置好清屏顏色之后,我們使用 glClear
來清除指定的緩沖區。在這個例子中,我們只需要清除顏色緩沖區即可,所以傳入的是 GL_COLOR_BUFFER_BIT
。
其他的緩沖區像 GL_DEPTH_BUFFER_BIT
(Z 緩沖)、GL_STENCIL_BUFFER_BIT
(模板緩沖)、GL_ACCUM_BUFFER_BIT
(累積緩沖)暫時都用不到,所以不用理會。
調用 glClear
之后,OpenGL 會用我們剛剛設置好的顏色去清空整個顏色緩沖區,也就是把后備緩沖區填滿指定的顏色。
第三步:交換前后緩沖區(SwapBuffers)
雖然我們已經在緩沖區里繪制了內容(例如清屏),但還沒有真正顯示在窗口上。OpenGL 默認使用雙緩沖,也就是“前緩沖區”顯示在屏幕上,“后緩沖區”用于繪制。繪制完成后,要調用 SwapBuffers
,把后緩沖區的內容切換到屏幕上顯示。
這時候需要傳入一個 HDC(設備上下文句柄),也就是窗口對應的 Windows 設備上下文,用于告訴系統具體要在哪個窗口顯示結果。
補充:設置視口(glViewport)
還有一個小細節是設置視口(viewport),即告訴 OpenGL 當前要渲染的區域在窗口上的哪一塊。這個通過調用 glViewport
實現,參數是左上角位置 (x, y)
和寬高 (width, height)
。通常我們會設置為窗口的整個區域,比如 (0, 0, width, height)
,表示從左上角開始,覆蓋整個窗口。
小結:
所以我們插入的 OpenGL 渲染流程是這樣的:
glViewport(0, 0, width, height)
:設置繪制區域為整個窗口。glClearColor(r, g, b, a)
:設置清屏顏色。glClear(GL_COLOR_BUFFER_BIT)
:清空顏色緩沖區。SwapBuffers(hdc)
:將結果顯示到窗口上。
這樣,每一幀我們就能看到一個由 OpenGL 渲染出來的純色畫面,這也是驗證 OpenGL 初始化是否成功的最簡單方式。雖然這個渲染操作非常基礎,但它建立了后續更復雜渲染流程的基礎。
運行游戲并注意到 “沒有出現粉色屏幕”
盡管按照我們剛才的步驟設置了 OpenGL 清屏顏色為粉色,但實際上我們并沒有看到粉色屏幕。這是因為,盡管我們按照正確的步驟進行了設置,但仍然缺少一些必要的操作來確保屏幕能正確顯示。
問題的根源在于,雖然我們做了 glClearColor
和 glClear
來設置和清空顏色緩沖區,但 OpenGL 渲染并不自動顯示內容,特別是在雙緩沖的情況下。我們已經設置了清屏顏色,并且清空了緩沖區,但并沒有真正把緩沖區的內容展示出來。
為了解決這個問題,除了清除顏色緩沖區外,還需要確保我們使用 SwapBuffers
來將渲染內容從后臺緩沖區(后緩沖)切換到前臺緩沖區(即屏幕顯示的部分)。如果沒有調用 SwapBuffers
,屏幕上就不會顯示任何內容,盡管后臺已經完成了渲染操作。
所以,雖然我們做了 glClear
和 glClearColor
的設置,結果卻沒有顯示預期的粉色屏幕。這說明我們在渲染的過程中仍然缺少了關鍵步驟——交換緩沖區操作(SwapBuffers
)。如果這些步驟沒做好,OpenGL 渲染的結果就不會正確顯示在屏幕上。
win32_game.cpp: 調用 Win32InitOpenGL,運行游戲并遇到無效代碼路徑
我們在程序中設置好 OpenGL 渲染流程,包括創建渲染上下文、設置清屏顏色、調用清除緩沖區以及進行緩沖區交換(SwapBuffers
),理論上應該可以在窗口中看到一塊粉色的清屏顏色。
但實際上,程序連渲染的那一步都沒有走到,甚至在初始化階段就失敗了。進一步檢查發現,是 OpenGL 渲染上下文(RC)創建失敗了。也就是說,wglCreateContext
這一關鍵步驟沒有成功執行,從而導致后續的 OpenGL 渲染都無法進行。
這比“屏幕沒有變成粉色”還要更直接地說明問題存在,因為程序根本沒有成功完成渲染上下文的建立,自然也不會進入到真正的繪制階段。
因此問題出在更早的地方,而不是渲染邏輯本身。這提示我們需要進一步檢查為什么渲染上下文創建失敗。接下來的任務就是找到導致渲染上下文創建失敗的根本原因,通常這與設備上下文(DC)未配置正確的像素格式有關,也可能是窗口本身尚未準備好進行 OpenGL 初始化。
總之,當前的問題并不是繪制邏輯失敗,而是根本沒有成功初始化 OpenGL,這才導致窗口中什么都沒有顯示出來。我們接下來要解決的就是為什么 RC 創建失敗的問題。
網上搜索: PIXELFORMATDESCRIPTOR1
為了讓 OpenGL 成功在窗口上進行渲染,我們不能只創建一個渲染上下文(Rendering Context)然后直接使用。必須先做一件非常關鍵的初始化操作:為設備上下文(DC)設置一個合適的像素格式(Pixel Format)。如果不做這一步,OpenGL 渲染上下文的創建是一定會失敗的。
這是一個遺留設計造成的問題,起因可以追溯到早年圖形系統資源極度受限的時代。那時候,顯存稀缺,帶寬昂貴,不像現在可以隨便用 32 位真彩色。系統甚至支持運行在調色板模式(如 8 位顏色,每個像素代表調色板中的一個顏色索引)或 16 色模式。因此,在進行圖形渲染時,操作系統需要明確知道我們期望的像素格式。
為此,我們需要調用 SetPixelFormat
,為窗口關聯的設備上下文(DC)設置一個支持 OpenGL 渲染的像素格式。然而,這并不是直接“指定”一個格式,而是一個“協商過程”。
我們需要構造一個 PIXELFORMATDESCRIPTOR
結構體,這個結構體里填滿了各種參數,比如希望使用的顏色深度(如 32 位 RGBA)、是否支持雙緩沖、是否支持 OpenGL、是否需要深度緩沖、模板緩沖等等。
但我們不能就這樣隨便填一個 PIXELFORMATDESCRIPTOR
然后直接使用。因為 OpenGL 的實現和底層的硬件驅動只支持一部分具體的像素格式。我們必須從系統實際支持的格式中選擇一個最接近我們期望的。
為此有兩個方法:
-
窮舉法:調用
DescribePixelFormat
,一個一個地查詢系統支持的所有像素格式,直到找到一個和我們期望匹配的為止。這種方式繁瑣,而且系統可能支持上百種格式。 -
選擇法(推薦):構造一個希望的
PIXELFORMATDESCRIPTOR
,然后調用ChoosePixelFormat
讓操作系統在它支持的格式中選出一個最接近我們期望的。這種方式更高效,也更可靠。
完成上述步驟后,還必須用 SetPixelFormat
將這個選中的像素格式正式設置到我們的設備上下文上。只有這樣,后續創建 OpenGL 渲染上下文的調用才會成功。
總結:
- 初始化 OpenGL 前,必須為窗口的 DC 設置像素格式。
- 設置像素格式涉及到一個“協商”過程,操作系統和硬件會選擇一個它們支持的格式。
- 使用
ChoosePixelFormat
+SetPixelFormat
是最常見、最穩妥的方式。 - 忽略這一步會導致 OpenGL 無法工作,即使后續的渲染代碼寫得完全正確,依然無法看到任何輸出。
這一步是使用 OpenGL 在 Windows 平臺上渲染的一個重要前提。
win32_game.cpp: 初始化該結構體
我們在初始化 OpenGL 時,還需要完成一個非常關鍵的步驟:設置像素格式(Pixel Format)。這部分看起來非常瑣碎,但它是整個 OpenGL 啟動過程中不可或缺的一步。我們不能直接使用 OpenGL 渲染上下文,必須先告訴 Windows,我們希望以什么樣的像素格式在窗口中進行渲染。
首先,我們要準備一個 PIXELFORMATDESCRIPTOR
結構體,用于描述我們希望使用的像素格式。這個結構體的設置要非常小心:
nSize
是結構體的大小,這個字段雖然沒什么用,但 Windows 要求必須設置。它的存在是為了“兼容將來的擴展”。nVersion
是版本號,目前只存在一個版本,設為1
。dwFlags
是標志位,我們必須啟用以下幾個標志:PFD_DRAW_TO_WINDOW
:表示可以渲染到窗口上。PFD_SUPPORT_OPENGL
:表示支持 OpenGL 渲染。PFD_DOUBLEBUFFER
:表示使用雙緩沖(back buffer + front buffer)。
然后是像素格式具體參數的設置:
iPixelType
應設為PFD_TYPE_RGBA
,表示使用 RGBA 顏色。cColorBits
設置為 32,表示使用 32 位顏色深度(24 位顏色 + 8 位 Alpha 通道)。cAlphaBits
設置為 8,表示我們需要 8 位 Alpha 通道。
其他如深度緩沖、模板緩沖、累積緩沖等,我們當前不需要,因此設置為 0。
我們并不能直接用這個結構體調用 SetPixelFormat
,因為 Windows 和底層硬件并不一定支持我們指定的格式。我們需要通過 ChoosePixelFormat
讓系統“推薦”一個最接近我們設定的像素格式。
這個函數會返回一個索引(整數),表示一個系統支持的像素格式。然后我們用這個索引調用 DescribePixelFormat
,獲取該像素格式的完整描述結構體(系統實際支持的那一個)。這一步非常重要,因為我們不能直接使用自己構造的結構體作為最終設定的格式,必須使用系統返回的版本。
最后,調用 SetPixelFormat
,把獲取到的像素格式應用到窗口的設備上下文(DC)上。這一步成功之后,我們才能繼續使用 wglCreateContext
創建 OpenGL 渲染上下文,并用 wglMakeCurrent
將其設置為當前上下文。
在實際執行時我們發現:
- 系統返回的格式確實是 32 位顏色,但包含了我們沒指定的緩沖區,比如 24 位的深度緩沖、8 位模板緩沖等。這可能是硬件默認的格式,只能接受有這些緩沖的版本,我們也不能排除或控制這些額外部分。
- Windows 的文檔中提到
cColorBits
不包含 Alpha 位,但我們實際測試時發現包含了 Alpha 位。說明文檔和實現并不一致。雖然文檔寫的是“excluding alpha”,但返回的是 32 位(含 Alpha)。我們根據實際行為把cColorBits
設置為 32 位,確保拿到我們想要的結果。 - 設置完成后我們成功創建了 OpenGL 上下文,這代表像素格式協商成功,設備上下文可以正常進行 OpenGL 渲染。
額外注意事項:
- 啟用 OpenGL 后,原本使用 GDI 實現的窗口特效(如淡入淡出)會失效。這是因為 OpenGL 接管了渲染流程,不再響應原有的窗口層疊或過渡效果。
- 如果之后需要使用 OpenGL 的 3.0 或 4.0 高級特性,還需要使用擴展方式創建上下文(如
wglCreateContextAttribsARB
),這是更高版本的上下文創建方式,我們可以以后再添加這部分。
總結:
- 初始化
PIXELFORMATDESCRIPTOR
并設置關鍵參數。 - 使用
ChoosePixelFormat
獲取最匹配的像素格式索引。 - 使用
DescribePixelFormat
獲取真實格式描述。 - 使用
SetPixelFormat
將其應用到設備上下文上。 - 創建 OpenGL 上下文并綁定。
完成以上步驟后,就可以開始使用 OpenGL 進行渲染。雖然過程繁瑣,但每一步都是確保 OpenGL 成功運行的關鍵。
Windows 下初始化 OpenGL 的關鍵步驟之一:設置像素格式(Pixel Format),我們逐行解釋這段代碼是做什么的,以及它為什么這么做。
HDC WindowDC = GetDC(Window);
作用:
- 獲取窗口的設備上下文(DC, Device Context),用于之后的圖形繪制。
Window
是一個窗口句柄(HWND),通過GetDC
獲取其對應的繪圖上下文。- 所有后續關于像素格式、OpenGL 上下文的創建都要用到這個 DC。
PIXELFORMATDESCRIPTOR DesiredPixelFormat = {};
DesiredPixelFormat.nSize = sizeof(DesiredPixelFormat);
DesiredPixelFormat.nVersion = 1;
DesiredPixelFormat.dwFlags = PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW | PFD_DOUBLEBUFFER;
DesiredPixelFormat.cColorBits = 32;
DesiredPixelFormat.cAlphaBits = 8;
DesiredPixelFormat.iLayerType = PFD_MAIN_PLANE;
作用:
- 創建一個我們希望使用的像素格式描述結構體(
PIXELFORMATDESCRIPTOR
)。 - 設置了我們對這個像素格式的要求,包括:
字段 | 含義 |
---|---|
nSize | 結構體大小,必須設定,Windows 要求 |
nVersion | 固定為 1 ,目前只存在這一版本 |
dwFlags | 設置三種標志:支持 OpenGL、可以繪制到窗口、使用雙緩沖 |
cColorBits | 希望的顏色深度,這里是 32 位(通常包含 RGB 和 Alpha) |
cAlphaBits | Alpha 通道的位數,8 位 |
iLayerType | 設置為主圖層(PFD_MAIN_PLANE ),也就是正常顯示層 |
int SuggestedPixelFormatIndex = ChoosePixelFormat(WindowDC, &DesiredPixelFormat);
作用:
- 調用系統函數
ChoosePixelFormat
,告訴它我們“理想”的像素格式是什么。 - 系統會返回一個最接近我們要求的像素格式的索引,因為我們不能直接使用自己的設定。
- 這個返回值
SuggestedPixelFormatIndex
是后續操作的關鍵。
PIXELFORMATDESCRIPTOR SuggestedPixelFormat;DescribePixelFormat(WindowDC, SuggestedPixelFormatIndex, sizeof(SuggestedPixelFormat),&SuggestedPixelFormat);
作用:
- 用
DescribePixelFormat
獲取上一步系統建議的像素格式的真實描述。 - 因為我們只拿到了一個索引,但并不知道它具體長什么樣。
- 通過這個函數,我們拿到系統真實支持的
PIXELFORMATDESCRIPTOR
,保存在SuggestedPixelFormat
中。
SetPixelFormat(WindowDC, SuggestedPixelFormatIndex, &SuggestedPixelFormat);
作用:
- 真正把我們選定的像素格式應用到窗口的設備上下文中。
- 這一步是必須的,不設置像素格式就不能創建 OpenGL 上下文。
- 必須傳入我們獲取到的格式索引和完整的格式描述結構體。
總結流程圖:
[ GetDC ] → 獲取窗口設備上下文(HDC)↓
[ 填寫 DesiredPixelFormat ] → 想要的像素格式↓
[ ChoosePixelFormat ] → 讓系統挑選最匹配的格式(返回索引)↓
[ DescribePixelFormat ] → 獲取索引對應的實際格式描述↓
[ SetPixelFormat ] → 應用格式到窗口 DC
完成這一步之后,我們的窗口就具備了 OpenGL 渲染的基礎條件,下一步就可以創建 OpenGL 上下文了。這個過程是所有 Windows OpenGL 程序的通用啟動流程。
問答環節
很有趣,粉色屏幕沒有出現在直播中,是 OBS 問題嗎?
我們在運行程序時遇到一個問題:在屏幕上確實渲染出了粉紅色的內容,但在通過 OBS 進行直播推流時,粉紅色部分完全沒有顯示,觀眾看到的畫面就是一片黑。這說明 OBS 并沒有成功捕捉到 OpenGL 渲染出來的圖像。
我們初步推測問題出在 OBS 和 OpenGL 的兼容性上。OBS 默認使用的是一種屏幕捕捉方式,它可能無法正確讀取由 OpenGL 繪制的圖像,尤其是如果 OpenGL 使用了某些特殊上下文、窗口設置或者硬件加速路徑。也有可能 OBS 在捕捉圖像的時候沒有處理好雙緩沖機制或者上下文切換,導致幀緩沖區的內容沒有被讀取。
更具體一點:
- OpenGL 在窗口中直接渲染圖像,它可能并不會走標準的 GDI 或 DWM 渲染路徑;
- OBS 捕捉的是系統合成之后的畫面,如果我們用的窗口不是標準合成層,或者渲染時用了某種“獨占”方式,OBS 可能根本捕捉不到;
- 一些顯卡或驅動(尤其是獨顯/集顯混用的系統)會把 OpenGL 渲染做在一個無法被系統正常捕捉的離屏表面上;
- 在 OBS 的設置中,如果沒有選擇正確的捕捉模式(例如“游戲捕捉”、“窗口捕捉” vs “顯示捕捉”),OpenGL 內容經常會丟失或黑屏;
- 某些系統下,使用硬件加速或全屏獨占窗口模式也會導致 OBS 捕捉不到任何內容。
最終結果就是:我們確實成功渲染了 OpenGL 圖像,但在 OBS 推流里卻什么都沒有顯示出來,看上去就像黑屏一樣。這種現象其實很常見,在調試 OpenGL 應用直播或錄制時需要特別注意工具之間的兼容性。可能需要切換 OBS 捕捉模式、關閉全屏獨占,甚至使用插件或強制啟用兼容性捕捉才能解決。
根據這個,你應該為 ColorBit2 使用 32
我們根據實際運行的結果觀察,發現文檔中關于 cColorBits
的描述似乎是錯誤的。文檔中明確寫著 cColorBits
不包含 alpha 通道的位數,意思是如果我們希望獲得 24 位顏色和 8 位 alpha,那應該設置 cColorBits = 24
和 cAlphaBits = 8
。
但是,實際情況是我們傳入 cColorBits = 24
后,系統返回的像素格式結構中,顏色位數是 32,alpha 位是 8。也就是說,系統實際上把 alpha 包含在了 color bits 里。說明 cColorBits
的真實含義在實際運行中是包括 alpha 的,或者說系統的行為根本沒遵循文檔所說的邏輯。
進一步判斷,在現代圖形硬件上,沒有哪塊顯卡會默認給出“32 位顏色再加 8 位 alpha”的配置,也就是說,所謂的 40 位顏色緩沖幾乎不可能是默認選項。因此可以確認,返回的 32 位顏色其實是包括了 8 位 alpha 的,也就是 RGBA 各 8 位。這是目前硬件最常用也最合理的格式。
總結如下:
- 系統返回的像素格式表明
cColorBits = 32
實際包括了 alpha; - 設置為 24 位 color + 8 位 alpha 并不會返回 24,而是 32,說明系統行為和文檔不一致;
- 主流顯卡不會默認使用超過 32 位的顏色緩沖,因此可以確認 alpha 是包含在 color bits 中的;
- 因此在設置
PIXELFORMATDESCRIPTOR
時,cColorBits
應該直接設為 32,不需要再額外考慮 alpha 是否被計入。
這說明在處理 OpenGL 初始化時,不能完全依賴文檔的說法,還必須根據實際返回的像素格式去驗證系統行為。我們最終采取的策略是直接設為 32,以確保得到我們期望的 RGBA 8888 格式。
想知道是 CPU 還是 GPU 實際上將信息傳輸到屏幕上。我記得你提到過這個,但我忘了
目前的渲染流程大致如下:
我們在 CPU 端打包一系列的渲染命令,這些命令并不會在 CPU 上被實際執行,而是通過 PCI 總線傳輸到 GPU。當命令傳輸過去后,GPU 接收到這些指令并按照其中的描述去執行實際的圖形操作,比如清屏、繪制幾何體、貼圖等等。
以“清屏為粉色”為例:
- 我們在 CPU 端構建了一個“清屏并設為粉色”的命令;
- 這個命令通過總線被發送到 GPU;
- 清屏操作并不在 CPU 上執行,而是由 GPU 在接收到命令之后進行;
- 所以最終屏幕呈現出粉色背景,是 GPU 實際完成了這一繪制任務。
換句話說,我們通過 CPU 指定“要做什么”,但真正“做這件事”的,是 GPU。兩者通過顯卡驅動和硬件接口進行協同工作。只要理解這一點,就可以清楚為什么某些操作在視覺上看起來“延遲”或和 CPU 無關,因為它們是異步在 GPU 上完成的。這種設計可以充分利用 GPU 的并行計算能力,提高整體渲染效率。
你對 Vulkan 有什么看法?
目前我們對 Vulkan 的態度偏向不喜歡,但由于某些限制,暫時還無法對其進行具體討論,可能是因為還未正式發布或者處于某種保密階段。因此我們需要等待合適的時機,才能公開討論 Vulkan 的具體內容或細節。
如果你有心做一個完全無關的教程直播,IO 完成端口會很好,因為我懶得看這方面的資料
我們認為 IO 完成端口(IO Completion Ports)是 Windows 系統中為數不多設計良好的 API 之一,非常值得了解和使用。雖然目前事情較多,可能沒有機會專門制作相關的教學,但我們仍然推薦花時間學習這套機制。
IO 完成端口是一種高效的異步 I/O 機制,適用于需要處理大量并發 I/O 請求的程序,特別是在服務器場景下表現出色。它允許我們將多個 I/O 請求與一個端口關聯起來,操作系統在請求完成時會通知我們,可以極大減少線程切換和上下文開銷,從而提升性能和可擴展性。
這套 API 提供了一種線程池模型,由我們控制線程的最大數量,而不是為每一個連接或請求都創建一個線程,這避免了線程爆炸的問題。整體上,它是 Windows 平臺上進行高性能網絡編程或文件處理的推薦方案之一。
你怎么看待 Nvidia GeForce 不清除內存的做法?
關于 NVIDIA GeForce 顯卡,如果說它不清除內存的意思,可能是指顯卡在使用過程中并不會主動清理顯存中的數據。這通常意味著顯卡會繼續保留數據在顯存中,直到系統或驅動程序決定清理它。
顯卡的內存管理是由 GPU 驅動控制的,一般來說,GPU 會在渲染過程中動態分配和使用顯存,但并不會主動清理顯存中的數據,尤其是當 GPU 還需要這些數據時。直到有新的渲染任務或系統需要釋放顯存時,顯存中的數據才會被清除或覆蓋。
這可能影響一些程序的表現,尤其是在內存壓力較大的情況下。如果顯卡的內存沒有被及時清理,可能會導致顯存的不足,從而影響圖形渲染性能或程序的穩定性。所以,在開發圖形程序時,需要考慮如何有效地管理顯存,避免過多無用數據占用顯存資源。
進得晚了。我們現在加載了自己的 GL 函數指針嗎?
目前并沒有加載 OpenGL 的函數指針,因為當前僅使用 OpenGL 1.x 的功能。在 Windows 上,OpenGL 1.x 的基本功能已經直接內建,所以只需要調用這些內建的功能就可以進行渲染,不需要額外的函數指針。
然而,如果將來需要使用 OpenGL 3.0 或 4.0 的功能,比如更先進的圖形渲染特性,就需要加載相應的函數指針。這是因為這些版本的 OpenGL 引入了更多的功能和擴展,需要通過動態加載函數指針來訪問這些功能,而不是依賴操作系統自帶的舊版本函數。
嘗試一種不同于粉色的顏色?OBS 可能把那個當作透明處理
考慮到捕捉問題,可能會將其處理為透明,但由于無法通過捕捉工具捕捉 OpenGL 輸出,因此如果將其設置為 OpenGL 捕捉模式就能解決這個問題。然而,最終決定使用捕捉卡來解決顯示問題,這樣就不再需要考慮捕捉相關的問題。這樣做的好處是,不僅解決了捕捉無法正常工作的難題,還能避免 CPU 始終處于 11% 負載的情況,從而優化了性能。
你知道為什么他們棄用了 GL_ALPHA 嗎?
在討論時提到的“GL_ALPHA”似乎是指某個特定的 OpenGL API。具體來說,這可能與 OpenGL 中的 alpha 渲染或透明度相關功能有關。不過,關于為什么這個 API 被棄用并沒有明確說明,可能是因為它在實際應用中的使用不再廣泛,或者由于技術進步和新的 API 替代了舊的實現方式。
如果是在談論 OpenGL 或相關圖形庫的變動,通常是因為舊的功能存在局限性,或者已經被更高效、更現代的方式所取代。因此,開發者通常會選擇棄用不再必要或被更好技術所替代的部分,以提高整體性能或簡化 API 的使用。
你不需要調用 DescribePixelFormat,因為當你使用 ChoosePixelFormat 時,它會自動修改 DesiredPixelFormat 中的內容
在使用 ChoosePixelFormat
函數時,它并不會修改我們傳入的 DesiredPixelFormat
結構體的內容。我們傳遞給它的是一個像素格式描述符(PIXELFORMATDESCRIPTOR
),它只是用于指定我們期望的像素格式參數。ChoosePixelFormat
函數會根據這些參數從系統中選擇一個合適的像素格式,并返回一個建議的像素格式索引。這個返回的索引指向系統推薦的像素格式,但它不會直接修改我們傳入的 DesiredPixelFormat
結構體本身。
因此,調用 ChoosePixelFormat
后,我們需要通過 DescribePixelFormat
函數來獲取具體的像素格式描述符,這時它才會填充我們傳入的 PIXELFORMATDESCRIPTOR
結構體,這個結構體才會包含實際的、被系統推薦的像素格式的詳細信息。
既然我們將使用深度緩沖區,那 Z 排序還重要嗎?
排序 Z 緩沖區(Z-sort)在渲染過程中仍然非常重要,原因有兩個:
-
透明效果的正確性:如果想要透明物體正確渲染,必須進行排序。透明物體需要按照從遠到近的順序進行繪制,以確保它們能夠正確地與背景進行混合,避免渲染錯誤。
-
性能提升:即使使用 Z 緩沖區,通過前到后的繪制順序可以優化渲染過程。在繪制時,可以使用早期的 Z 測試(early Z),跳過那些已經被遮擋的物體,從而節省 GPU 的計算資源,提高渲染速度。
此外,不使用 Z 緩沖區還能節省帶寬,因為沒有必要進行 Z 緩沖區的讀寫操作,從而減少內存訪問的開銷。不過,即使如此,仍然有可能開啟 Z 緩沖區來利用早期遮擋檢測(early occlusion),進一步優化性能。
總的來說,雖然在某些情況下可以不使用 Z 緩沖區,但在渲染透明物體時,排序 Z 緩沖區仍然是非常重要的,且能帶來性能的提升。
抱歉!看起來 OpenGL 文檔又在騙人
OpenGL 的文檔似乎經常不準確,已經多次驗證了這一點。盡管文檔中有很多描述,但在實際應用中,遇到的情況常常與文檔所說的不一致,這讓解決問題變得更加困難。
你會使用多個 OpenGL 版本,還是只用最小的版本以兼容一般的 Windows XP 機器?(大多數機器上用的是什么版本?)
考慮到目標平臺為較老的 Windows XP 系統,選擇合適的 OpenGL 版本是關鍵。由于我們開發的是一款 2D 游戲,并不需要太多擴展功能,因此可以選擇一個合理的 OpenGL 版本,甚至可能僅使用 OpenGL 2.0,這樣就能滿足大部分需求,同時保持系統的輕量化。總體來說,盡量保持最小化,避免引入不必要的復雜功能,因為對于這個項目而言,并不需要太多額外的 OpenGL 功能。
對于像這樣的游戲,使用 OpenGL 的新版本會有什么好處嗎?
使用新的 OpenGL 版本對于像這種游戲可能會有一些好處,尤其是涉及到圖形渲染時。我們可能需要使用一些著色器(Shaders),至少會用到一些基礎的著色器。雖然不使用著色器也能運行,但為了實現更豐富的效果,比如特殊的視覺效果或性能優化,使用著色器會更合適。這樣可以提升游戲的視覺表現和靈活性,使得渲染效果更加多樣化。
調用 OpenGL 函數(比如 glClear())時,opengl32.lib 和顯卡驅動 DLL 之間有區別嗎?
在調用像 GL_Clear
這樣的函數時,無論是使用 OpenGL 32 庫中的版本,還是圖形驅動 DLL 中的版本,它們是完全一樣的。沒有任何區別。這意味著兩者的功能和效果是相同的,都是通過圖形驅動提供的 OpenGL 接口來執行的。
代碼庫與導入庫
當看到 .lib
文件時,它可能有兩種含義。它可以是一個導入庫(import library),也可以是一個代碼庫(code library)。例如,如果你鏈接的是像 ZLib
這樣的庫,那它是一個代碼庫,包含了一些實際的代碼,如壓縮代碼,它將被鏈接到游戲中。而 OpenGL32.lib
不是這種代碼庫,它是一個導入庫,類似于 user32.lib
,它不包含實際的代碼實現。
OpenGL32.lib
只是提供了與操作系統的綁定點。通過鏈接到 OpenGL32.lib
,你實際上是調用操作系統提供的 OpenGL 接口,而這個接口在程序運行時被加載。這個庫本身并不包含 OpenGL 的實際代碼,它只是將調用傳遞給操作系統的服務,然后操作系統會決定是否直接將調用轉發給圖形驅動程序,或者先經過一些額外的步驟,例如檢查或進行環轉移等操作。
因此,OpenGL32.lib
的作用只是提供與操作系統交互的綁定點,從而通過操作系統與圖形驅動程序進行通信。
既然我們使用 OpenGL,那么你會修改我們的 2.5D 特效,還是保持代碼不變?
代碼將保持不變,只是在未來會有一個硬件路徑。
對于 ChoosePixelFormat(),我們是不是不應該指定 iPixelType 并將其設置為 PFD_TYPE_RGBA?
在選擇像素格式時,可能沒有指定像素類型為PFD_TYPE_RGBA,結果可能是運氣好,默認值為0。如果沒有顯式指定,它可能會默認設置為0。
win32_game.cpp: 設置 DesiredPixelFormat.iPixelType
雖然沒有指定像素類型為PFD_TYPE_RGBA是 technically 正確的,因為默認值是0,并且這樣可以得到預期的結果,但為了代碼更清晰和符合規范,還是將其顯式寫出會更好。這樣可以確保代碼更加符合標準,也更易于理解。
所以通過鏈接 opengl32.lib,我們就不需要使用 GetProcAddress 來加載 OpenGL 嗎?
通過鏈接OpenGL32.lib并不意味著不需要使用GetProcAddress
。原因是,早期Windows系統自帶的OpenGL版本過舊。如果只使用OpenGL 1.x版本,確實不需要調用GetProcAddress
,類似于不需要為CreateWindow
調用它,因為Windows會直接提供這些功能。但如果使用較新的OpenGL版本,就必須通過GetProcAddress
來獲取相關函數指針。
Blackboard: 動態鏈接表
在編譯和鏈接程序時,有兩種常見的函數調用方式。第一種是直接調用自己定義的函數,比如myFunction
。這種函數在編譯后會被替換為一個相對地址的調用,編譯后的可執行文件中不再保留函數本身的代碼。第二種是調用操作系統提供的函數,如CreateWindow
。這種函數的調用不會直接指向實際地址,而是通過一個動態鏈接表(DLL)進行管理。在加載程序時,操作系統會將CreateWindow
的實際地址映射到程序的地址空間中,動態鏈接表會更新所有指向該函數的調用,替換為實際的函數地址。
GetProcAddress
是用來手動完成這種過程的工具。通常,如果是操作系統內置的函數(如CreateWindow
),程序可以依賴動態鏈接表來進行調用,但如果是可選的外部庫函數(如XInput或OpenGL的擴展函數),則需要使用GetProcAddress
來查找這些函數的地址,以便在程序運行時檢查并動態加載它們。這是為了確保程序能在不同的機器上運行,即使某些庫可能不存在。
例如,Windows系統上的OpenGL 1.x版本是固定的,不需要使用GetProcAddress
來調用,但對于更高版本的OpenGL(如OpenGL 2.0及以上),其中的新函數則需要通過GetProcAddress
來查詢和加載。
總結來說,GetProcAddress
和類似的機制允許程序在運行時動態加載函數和庫,這樣可以確保程序在沒有預先依賴某些庫的情況下也能正確運行。
當然可以,下面我們用實際代碼來舉幾個例子幫助理解上面講的內容,尤其是 GetProcAddress
在 OpenGL 和其他 Windows API 場景中的用途。
示例 1:普通函數調用(靜態鏈接)
void MyFunction() {// 執行一些邏輯
}int main() {MyFunction(); // 編譯器在編譯時已知地址,直接生成調用指令return 0;
}
說明:這是一個普通的靜態鏈接函數,編譯后會直接在可執行文件中生成指向 MyFunction
的地址,不涉及動態鏈接。
示例 2:調用 Windows API(通過 import lib)
#include <windows.h>int main() {HWND hwnd = CreateWindowA("STATIC", "Title", WS_OVERLAPPEDWINDOW,0, 0, 800, 600, NULL, NULL, NULL, NULL);return 0;
}
說明:CreateWindowA
是 Windows API,通過 user32.lib
鏈接。這類調用在程序啟動時由操作系統裝載并修復函數地址(通過 IAT - Import Address Table)。不需要手動調用 GetProcAddress
。
示例 3:手動加載函數(用于可選組件)
#include <windows.h>
#include <stdio.h>typedef DWORD (WINAPI *LPXInputGetState)(DWORD, void*);int main() {HMODULE xinput = LoadLibraryA("xinput1_4.dll");if (xinput) {LPXInputGetState XInputGetState = (LPXInputGetState)GetProcAddress(xinput, "XInputGetState");if (XInputGetState) {printf("XInputGetState 加載成功,可以使用手柄功能\n");} else {printf("XInputGetState 加載失敗,禁用手柄功能\n");}} else {printf("XInput DLL 未加載,系統不支持 XInput\n");}return 0;
}
說明:使用 GetProcAddress
是因為某些 Windows 系統可能沒有安裝 XInput。這樣可以讓程序在沒有 XInput 的機器上仍然正常運行(只是不支持手柄)。
示例 4:加載 OpenGL 高版本函數(OpenGL 擴展)
#include <windows.h>
#include <GL/gl.h>
#include <stdio.h>typedef void (APIENTRY *PFNGLGENBUFFERSPROC)(GLsizei, GLuint*);
PFNGLGENBUFFERSPROC glGenBuffers;void LoadOpenGLFunctions() {glGenBuffers = (PFNGLGENBUFFERSPROC)wglGetProcAddress("glGenBuffers");if (glGenBuffers) {printf("glGenBuffers 加載成功\n");} else {printf("glGenBuffers 加載失敗,可能不支持 OpenGL 1.5+\n");}
}
說明:glGenBuffers
是 OpenGL 1.5 中引入的函數,Windows 系統只內置了 OpenGL 1.1,所以必須使用 wglGetProcAddress
動態加載。否則會導致鏈接失敗或運行時報錯。
🔚 小結
情況 | 是否需要 GetProcAddress(或類似) |
---|---|
普通 C 函數 | ? 不需要 |
Windows 標準 API(如 GDI) | ? 不需要 |
可選 DLL 函數(如 XInput) | ? 需要(GetProcAddress) |
OpenGL 擴展函數(1.2+) | ? 需要(wglGetProcAddress) |
需要動態加載的函數一般都屬于操作系統可選功能或第三方庫擴展功能,用 GetProcAddress
可以讓程序更靈活兼容。
我們來詳細講一下操作系統在加載一個使用了 DLL 的可執行程序時,如何通過 IAT(Import Address Table,導入地址表) 來修復函數地址的機制。
背景知識
在 Windows 中,可執行程序(EXE)和動態鏈接庫(DLL)采用的是 PE(Portable Executable)格式。在編譯時,如果我們使用了某個 DLL 的函數(例如 CreateWindow
來自 user32.dll
),編譯器會把這些函數的符號記錄到 PE 文件中,但并不直接寫死它的內存地址——因為 DLL 的地址在運行時才知道。
核心概念
🔸 Import Table(導入表)
- 包含當前可執行文件依賴的 DLL 列表。
- 每個 DLL 名下列出使用了哪些函數。
🔸 IAT(Import Address Table,導入地址表)
- 是一張指針表,運行時會被操作系統填充成真實的函數地址。
- 程序中對 DLL 函數的調用,實際上是通過訪問 IAT 來進行的。
加載流程(按步驟詳細解釋)
編譯時
假設我們寫了如下代碼:
#include <windows.h>int main() {MessageBoxA(NULL, "Hello", "Title", MB_OK);return 0;
}
編譯后:
- 編譯器知道你用了
MessageBoxA
,來自user32.dll
。 - 在
.idata
節中寫入:- 需要導入的 DLL 名稱(如
user32.dll
) - 函數名稱列表(如
MessageBoxA
)
- 需要導入的 DLL 名稱(如
- 在
.rdata
或.idata
區域生成一張 IAT 表,最開始每項都是 0 或指向函數名的 thunk。
程序啟動時(由 Windows 的 Loader 完成)
-
映像映射
- 操作系統加載 EXE 文件到內存。
- 找到
.idata
區塊,識別出所需的 DLL。
-
加載 DLL
- 加載
user32.dll
(若尚未加載)。 - 記錄該 DLL 的加載基地址。
- 加載
-
修復導入地址表(填充 IAT)
- 對于每一個需要的函數(如
MessageBoxA
):- 在 DLL 的導出表中查找它的地址。
- 將找到的地址寫入到當前 EXE 的 IAT 表中相應位置。
- 對于每一個需要的函數(如
-
跳轉調用
- 當程序調用
MessageBoxA
時,其實是通過 IAT 中的函數指針跳轉過去的。
- 當程序調用
舉個實際例子
假設我們寫了:
MessageBoxA(...);
實際執行時類似:
CALL DWORD PTR [0x00402000] ; 假設這是 IAT 中 MessageBoxA 的入口
這個地址在程序運行前是空的,運行時會變成:
0x00402000 => 0x77D4A390 ; 實際指向 user32.dll 中 MessageBoxA 的地址
📎 延遲加載(Delay Load)
一種優化方式:不要在程序啟動時就加載所有 DLL,而是等到函數被真正調用前才加載。
編譯器支持 /DELAYLOAD:user32.dll
,配合內部的跳板代碼實現這種延遲修復。
IAT 和安全
優點:
- 靈活支持 DLL 升級和系統兼容。
- 可以延遲綁定,減小啟動開銷。
缺點:
- 惡意軟件可能會篡改 IAT 實現鉤子(Hooking),用于注入或監聽函數調用(例如鍵盤鉤子等)。
可視化工具推薦
你可以用這些工具來查看一個 EXE 的 IAT:
- Dependency Walker(depends.exe):經典工具。
- PE-bear / CFF Explorer:現代 UI,支持查看導入表結構。
既然你經常抱怨 Windows,為什么不使用其他操作系統?
我們經常會抱怨 Windows,但仍然選擇使用它的主要原因是作為游戲程序員,幾乎是被動地必須使用 Windows。當前大約 95% 的游戲市場都運行在 Windows 平臺上,這是一個壓倒性的份額。
無論我們是否喜歡這個系統,都無法改變這個現狀。在當前的市場結構下,如果目標是開發面向主流用戶的游戲,那就必須在 Windows 上進行開發和測試。除非其他操作系統的市場份額總和能達到 70% 甚至更高,否則沒有現實的選擇空間。
這就是為什么我們仍然堅持使用 Windows,即使它存在很多讓人頭疼的問題。開發環境、用戶基礎、驅動支持、圖形 API(如 DirectX 和 OpenGL)、兼容性和測試需求等因素,決定了這是游戲開發中難以回避的平臺。
https://store.steampowered.com/hwsurvey/Steam-Hardware-Software-Survey-Welcome-to-Steam
你認為為了 OS X 端口,是否需要寫一個用 Objective-C 的包裝器來使用 OpenGL 并創建窗口?
在 macOS 系統上開發程序時,如果要創建一個行為符合規范的應用程序,通常需要使用 Objective-C 編寫啟動部分。這并不僅僅是為了使用 OpenGL,而是整個 macOS 應用生命周期的管理都依賴于 Cocoa 框架,而 Cocoa 是基于 Objective-C 的。
要在 macOS 上創建窗口、處理事件、響應系統消息等,往往都需要調用 Objective-C 的類和方法。即便只想做一個簡單的圖形程序,也很難完全避開 Objective-C,尤其是在處理窗口創建和與操作系統交互時更是如此。
雖然現在也可以通過 Swift 或者 C 接口間接處理部分工作,但從技術實現和兼容性角度出發,Objective-C 仍然是構建原生 macOS 應用程序最常用的方式之一。因此,如果目標是開發一個在 macOS 上行為規范的程序,就必須引入 Objective-C 的啟動邏輯和封裝。
這可能是個愚蠢的問題,但如果我們使用 OpenCL 將大部分計算任務轉移到 GPU 上,而不使用 OpenGL,保持現有的做法會怎樣?
關于將現有大量計算任務通過 OpenCL 轉移到 GPU 上的設想,這種方式在實際中并不可行。并不是說只要把現有的軟件渲染器代碼交給 OpenCL 就能獲得更高的性能。實際上,GPU 執行通用代碼的效率遠低于 CPU。如果希望在 GPU 上獲得加速,必須有一整套專門為 GPU 架構優化的并行設計。
具體來說,需要將計算任務高度并行化,例如使用類似 32 通道(SIMD)寬度的操作方式,并將所有邏輯組織成適合 GPU 的并行執行單元。當前的架構雖然已經采用了瓦片化的渲染方式,在某種程度上有利于并行化,但為了在 OpenCL 中高效運行,仍需對現有代碼結構進行大量改造。
此外,即使付出如此代價,所獲得的收益也可能不如直接采用 OpenGL 渲染管線來得直接有效。與其花費大量精力重構原有渲染邏輯以適配 OpenCL,不如將相同的努力用在接入 OpenGL 這樣的圖形 API,通過圖形管線實現硬件加速渲染。這樣不僅更符合現代圖形渲染的標準流程,而且更穩定、通用性更好。因此,與其試圖通過 OpenCL 重構渲染器,不如直接使用 OpenGL 的圖形渲染服務來更合理地完成目標。
在休息期間,我寫了一個程序,能夠通過掃描 cpp/h 文件來生成 OpenGL 函數指針的聲明和初始化。你打算在中做類似的事情,還是保持簡單?
在項目中,對于 OpenGL 函數指針的初始化,通常可以通過掃描 .cpp
和 .h
文件來自動生成需要的裝飾和初始化代碼,從而確保所有使用到的 OpenGL 擴展函數都被正確加載。在這個過程中,工具會根據源碼中用到的 OpenGL 函數名自動分析并生成對應的函數指針聲明和加載邏輯。
不過,在當前這個游戲項目中,我們會選擇保持簡單,并不會使用這種自動化生成的復雜機制。我們將手動處理這些函數指針的初始化,保持代碼清晰和可控。雖然在更復雜或大型的項目中,會更傾向于使用自動工具來處理這類重復性和容易出錯的任務,但在這種體量較小、目標明確的項目里,簡單直接的做法更加高效實用。
著色器中的 if 語句有多糟糕?
在著色器(shader)中使用 if
語句的效果好壞,取決于執行該著色器的所有像素是否走相同的分支路徑。在 GPU 上,這種分支行為可能會導致性能問題,原因如下:
GPU 是高度并行的處理器,多個像素(通常是一組稱為“warp”或“wavefront”的并行像素)會同時執行同一個著色器程序。如果這些像素在 if
判斷時走了不同的路徑,那么 GPU 不能真正做到“并行處理”所有分支,而是要依次執行每一個分支,并通過掩碼的方式讓某些像素“暫停”等待其他像素執行完當前分支。這被稱為“分支分歧”(branch divergence)。
如果所有像素都走了同一個分支,GPU 就可以高效地并行執行,不會產生性能損耗。但如果某些像素進入了 if
的 true 分支,另一些進入 false 分支,就必須分別處理兩條路徑,執行時間就會變長,效率降低。
因此,在編寫著色器時,應盡量避免在片元著色器中出現依賴于像素條件的 if
分支,除非能夠保證分支在空間上是一致的(例如整個屏幕區域都會滿足某個條件)。否則,這種看似簡單的條件判斷會對性能造成較大影響。總結就是:if
并不可怕,但在 GPU 上執行的時候,如果不同像素走了不同的路,那就“糟透了”。
你曾經為游戲機編程嗎?
曾經為游戲主機編寫過程序,不過大多數經驗是在較早期的主機平臺上。例如 Xbox、PlayStation 2 和 GameCube 等時代的主機。當時主要進行的是底層或跨平臺相關的工作。雖然最近也接觸過較新的主機,比如在某年十一月曾經短暫地為 PlayStation 4 編寫過大約一周的程序,但并不算深入參與。總體來說,主要的開發經驗還是集中在早期主機系統的開發流程與技術實現上。
Linux 比 Windows Vista 強多了
在討論操作系統時,Linux 的使用比例確實在某些方面超越了 Windows Vista。具體來說,雖然 Windows Vista 64 和 32 位的使用占比很小(大約在0.4%或0.5%之間),但整體來看,Linux 在某些情況下能超越 Vista,尤其是當統計所有 Linux 發行版的使用比例時,Linux 的市場份額就超過了 Vista。不過,這個數據看起來有點混亂,似乎有很多操作系統未被包含在統計中,因此最后的對比結果有些難以確認。總結來看,如果算上所有版本的 Linux,它的使用率確實可以擊敗 Vista,這是一個積極的趨勢。