游戲引擎學習第237天:使用 OpenGL 顯示圖像

win32_game.cpp: 禁用 PFD_DOUBLEBUFFER

我們正在處理一個新的開發階段,目標是在使用 OpenGL 渲染的同時能正常通過 OBS 進行直播。昨天我們已經嘗試了一整天來解決這個問題,希望能找到一種方式讓 OBS 能正確地捕捉到 OpenGL 的窗口畫面。雖然我們不確定是否已經徹底解決了問題,但今天打算繼續推進,試試看現在的設定是否能正常直播和顯示游戲畫面。

當前的目標是讓游戲的圖像正確地通過 OpenGL 顯示出來,并確保觀眾可以在直播中看到它。我們計劃直接動手實施這個過程,看看是否有效。如果畫面不能正常顯示,那就只能接受直播偶爾出問題的現實。

現在我們要做的第一件事就是讓直播能夠正常運行。昨天的調查結果是:如果關閉 OpenGL 的雙緩沖(Double Buffering)功能,OBS 就可以更可靠地捕捉窗口內容進行推流。因此我們嘗試在平臺層代碼中加入一個條件編譯或運行時標志,例如 HANDMADE_STREAMING 這樣的宏或變量。其作用是在開啟該標志時,我們就不啟用 PFD_DOUBLEBUFFER(雙緩沖),而是改為只用單緩沖模式進行渲染。

我們還在注釋中說明了這一點:PFD_DOUBLEBUFFER(雙緩沖)這個標志可能會阻止 OBS 正常地捕捉和推送窗口畫面,這似乎是目前發現的導致問題的關鍵。

最終的希望是我們昨天的發現能適用于大多數情況,即:關閉雙緩沖可以解決 OpenGL 畫面無法被 OBS 正確捕捉的問題,從而實現直播與 OpenGL 渲染的兼容共存。接下來,我們就以這個設定繼續開發和測試。

嘗試一下并查看我們的粉紅色窗口

我們現在來嘗試一下當前的設定,運行程序后,屏幕上成功顯示出了一個粉紅色的窗口。接下來的問題是——大家到底能不能在直播畫面中看到這個粉紅窗口?是否有人看到的是黑屏?是粉紅色?還是只是 Windows 的 Visual Studio 開發界面?

我們開始向觀眾確認當前直播畫面中能否正確顯示這個粉紅窗口。最終反饋結果是,確實可以看到粉紅色的窗口。這就說明我們通過關閉雙緩沖所做的調整目前是成功的,OBS 能夠正確捕捉并推送 OpenGL 渲染出來的內容。

這也驗證了我們昨天的假設:如果關閉 PFD_DOUBLEBUFFER,OBS 在捕捉窗口時就不會出問題,從而能在直播中正常顯示 OpenGL 渲染的畫面。我們的直播和圖形顯示終于可以同時運作,沒有互相干擾。

這是一個非常有趣也常見的技術現象,背后涉及到 OpenGL 的渲染機制OBS 的屏幕捕捉方式 之間的兼容性問題。我們來詳細拆解為什么關閉 PFD_DOUBLEBUFFER(雙緩沖)后 OBS 就能正常顯示畫面。


一、雙緩沖(PFD_DOUBLEBUFFER) 是什么?

在 OpenGL 或任何圖形渲染系統中:

  • 雙緩沖是指有兩個緩沖區(Front Buffer 和 Back Buffer):
    • Back Buffer(后緩沖):繪圖時內容渲染在這個緩沖上;
    • Front Buffer(前緩沖):屏幕實際顯示的內容來自這里。
  • 每一幀完成后,程序通過 SwapBuffers() 把后緩沖的內容“交換”到前緩沖,顯示在屏幕上。

好處是:

  • 畫面不會撕裂;
  • 能實現流暢、干凈的幀切換。

二、OBS 是怎么捕捉窗口的?

OBS 在捕捉窗口內容時,會嘗試讀取 系統顯示在屏幕上的幀緩沖內容,具體方式依賴于:

  • 操作系統提供的 GDI、DWM 機制;
  • 顯卡驅動如何暴露前臺窗口的顯示緩沖內容;
  • 有些時候是直接采集前緩沖區(Front Buffer)內容。

但問題就在這里:

當啟用雙緩沖時,OpenGL 默認把圖像畫到后緩沖里,而不是前緩沖。

如果你沒調用 SwapBuffers(),前緩沖就是空的,OBS 捕捉到的自然也是空白(黑屏)。


三、為什么關閉 PFD_DOUBLEBUFFER 反而 OBS 能顯示?

當關閉了 PFD_DOUBLEBUFFER

  • OpenGL 只使用一個緩沖區,也就是 單緩沖(Single Buffering);
  • 所有渲染直接寫入前緩沖;
  • 因此 OBS 看到的就是你正在渲染的內容

換句話說:

單緩沖 = 直接把畫面畫在屏幕上 = OBS 能捕捉到。


四、但是關閉雙緩沖的副作用是什么?

雖然 OBS 可以顯示,但這不是一個理想的解決方案。單緩沖會帶來很多問題:

問題說明
撕裂(Tearing)渲染過程和顯示過程重疊,可能看到“斷裂”的畫面
閃爍(Flickering)渲染未完成時屏幕就開始顯示,會造成視覺跳動
性能波動每次繪制都直接在屏幕上,GPU 沒法優化渲染流程

正確方案是什么?

長遠來看,更合適的方案是:

  • 保持雙緩沖;
  • 在渲染完成后 確保調用 SwapBuffers()
  • 使用 OBS 的專門支持 OpenGL 的插件或硬件采集卡
  • 或者使用 幀緩沖對象(FBO) 渲染,然后將其輸出到屏幕,同時 OBS 捕捉這個輸出。

總結一句話:

關閉 PFD_DOUBLEBUFFER 后,OpenGL 的渲染直接作用于前緩沖,OBS 才能看到。但這是一種權宜之計,生產環境下更推薦保持雙緩沖并配合正確的幀交換和捕捉方式。

回顧當前的情況

我們目前已經有了一個完整的軟件渲染器,因此在接入 OpenGL 的第一步,并不需要立刻將整個游戲都通過 OpenGL 來渲染。我們的目標僅僅是把我們已經在 CPU 端生成的圖像,也就是渲染緩沖區中的畫面,傳輸到顯卡上,并通過 OpenGL 顯示出來。

現在我們所做的事情非常簡單,僅僅是調用了 glClearColor 設置清除顏色為粉色,然后調用 glClear 執行清屏操作。因此當前 OpenGL 顯示的畫面就是一片粉色,這是因為我們只發出了清屏的指令,沒有告訴 GPU 要渲染任何其他東西。

OpenGL 的指令緩存(command buffer)目前僅包含:

  • 設置清除顏色(粉色);
  • 設置清除區域(窗口的大小范圍);
  • 指定清除哪一個緩沖(如顏色緩沖);
  • 然后提交這些指令去執行。

接下來我們想做的是,不僅僅只是顯示粉色清屏,而是把我們在內存中已經繪制好的游戲畫面,也就是一個位圖緩沖區(bitmap buffer),傳輸到顯卡,然后由顯卡負責顯示它。

這個過程本質上和我們寫的軟件渲染器非常相似,也正是我們當初要自己寫渲染器的原因:我們可以從中理解圖形卡(GPU)背后的工作方式。回顧我們寫軟件渲染器的過程,可以更好地理解 GPU 的邏輯。如果記不清了,也可以回去重新看一下我們早期實現的部分。

在 GPU 中,我們需要準備兩樣東西:

  1. 圖像數據源:也就是我們要傳輸的圖像,通常叫做“紋理”(texture),這是 GPU 用來讀取像素信息的對象;
  2. 繪制指令:也就是讓顯卡畫出圖像的方式,具體來說要繪制一種叫“圖元”(primitive)的圖形。圖元是 GPU 所支持的基本形狀,比如點、線、三角形等等。

在我們的渲染器中,我們唯一使用的圖元其實是矩形。而在 GPU 的世界中,最常用的圖元是三角形。為了在顯卡上畫出一個矩形,我們需要把它拆成兩個三角形組合成一個矩形形狀。

這就是我們接下來要做的事:

  • 把我們的圖像緩沖區上傳成一張 OpenGL 紋理;
  • 創建一個由兩個三角形組成的矩形;
  • 把這兩個三角形繪制出來,并讓它們使用我們上傳的紋理進行采樣,從而顯示出原始的圖像。
    在這里插入圖片描述

Blackboard: 繪制四邊形的方法

我們需要在屏幕上繪制矩形區域來顯示圖像,為了實現這一點,有兩種方法可以選擇:


第一種方法:用兩個三角形拼出一個矩形

我們可以通過繪制兩個三角形來組成一個矩形。因為 OpenGL 最基礎的繪圖單位是圖元(Primitive),最常見的圖元是三角形,GPU 最擅長處理的也是三角形。

我們將會給出六個頂點(每個三角形三個),構成兩個拼接起來的三角形,這樣就能完美組成一個矩形。這樣的方法通用性強,適用于任意位置的矩形繪制,也適合將來用來顯示我們所有的圖像精靈(Sprite)。


第二種方法:繪制一個大三角形然后進行裁剪(Clipping)

另一種思路是只繪制一個三角形,但使用 OpenGL 的裁剪功能把它裁剪成一個矩形形狀來顯示。這種方式可以使用 OpenGL 的裁剪功能(比如 glScissor 指定一個區域),讓三角形只在屏幕指定區域內顯示,其他部分被裁掉。

這類似于軟件渲染中我們做的“裁剪到屏幕邊界”的操作,但 OpenGL 的裁剪功能是更通用的,它允許我們裁剪到比屏幕更小甚至不規則的區域,裁剪的區域可以自定義。


但是我們不會使用第二種方法:

盡管用 glScissor 裁剪確實可以做到我們想要的效果,但我們暫時不會采用這種方式,原因如下:

  • 調用裁剪功能在某些顯卡上可能是一個較慢或昂貴的操作;
  • 設置裁剪區域可能會讓渲染流程變得更復雜;
  • 更重要的是,我們將來的目標是把整個游戲的渲染從軟件柵格化(軟件計算每個像素)遷移到 OpenGL 上去;
  • 到時候我們會有很多圖像精靈需要繪制,而每個圖像都需要在不同的位置繪制不同的矩形,這種情況下用兩個三角形拼出矩形更通用、更靈活。

所以最終選擇是:使用兩個三角形繪制矩形

我們將使用兩個三角形來表示矩形區域,這樣我們可以在不依賴任何裁剪操作的情況下自由繪制圖像,而且每個精靈(Sprite)都可以獨立控制顯示的位置、大小、貼圖等內容。

我們接下來將按這種方式來實現把 CPU 渲染好的圖像上傳到 GPU,并在 GPU 上通過 OpenGL 顯示出來的過程。這個方法是我們未來整個渲染系統遷移到 GPU 后的基礎。

Blackboard: 使用兩個三角形來繪制我們的四邊形紋理

整個流程跟我們之前在軟件光柵化器中所做的基本一模一樣,我們需要:


1. 構造矩形(由兩個三角形組成)

我們首先要做的,是在屏幕上構造一個由兩個三角形組成的矩形。這個矩形將作為圖像的顯示區域。它的頂點坐標會告訴 GPU 把圖像畫在屏幕的哪個位置。


2. 設置 UV 坐標(紋理坐標)

我們要給這個矩形的四個頂點設置對應的紋理坐標,也就是所謂的 UV 坐標。

  • U、V 是紋理坐標的兩個軸,范圍通常是 0 到 1;
  • UV 坐標的作用是告訴 GPU:這個頂點對應貼圖中的哪個位置;
  • 比如左上角是 (0,0),右下角是 (1,1),這樣整個貼圖就會剛好填滿整個矩形。

這和我們之前在 CPU 上自己寫的渲染器中做法完全一致。


3. 將貼圖加載進 GPU 內存

接下來我們需要把一張貼圖,也就是一張圖像,加載到 GPU 中。這張圖像是我們在 CPU 渲染器中已經生成好的那一張畫面。

加載的方式通常是使用 OpenGL 的 glTexImage2D 或其他相關函數,把像素數據從 CPU 端上傳到 GPU 的顯存中。


4. 渲染這個貼圖

當我們完成以上三步之后,我們就有了:

  • 一個在屏幕上的矩形區域;
  • 這個矩形的每個頂點都有對應的紋理坐標;
  • 一張已經加載好的貼圖;

現在我們只需要用 OpenGL 渲染這個矩形,GPU 就會自動用貼圖的內容來“填充”整個矩形區域,實現圖像顯示的目的。


最終效果

一旦完成這些步驟,屏幕上就會顯示出我們原本在 CPU 中渲染出來的那張畫面,但現在是由 GPU 通過 OpenGL 來負責顯示的。這就是我們遷移渲染工作的一小步,從 CPU 顯示到 GPU 顯示的關鍵節點。

這個過程是整個 GPU 渲染系統的基礎操作。只要能成功做出這一步,就可以在其基礎上繼續實現更復雜的 GPU 加速渲染。

win32_game.cpp: 解釋 glVertex 命名法

我們要做的第一步,是嘗試在屏幕上繪制一個矩形,這個矩形由兩個三角形拼接而成。這個階段我們不會貼圖,只先試著畫出純幾何形狀,確認 OpenGL 渲染管線是否正常工作。由于貼圖是一個更復雜的步驟,我們將其留到后面。


保持背景粉色

我們仍然保留背景為粉色的清屏操作(glClearColor + glClear),這樣做的好處是:

  • 可以非常直觀地看出我們畫上去的圖形是否真的顯示出來;
  • 如果矩形能正確覆蓋粉色背景,說明渲染路徑基本是通的。

使用 OpenGL 舊式固定功能管線繪制

接下來我們將采用 OpenGL 的**舊式渲染方式(Immediate Mode)**來繪制兩個三角形,也就是用 glBegin()glEnd() 來環繞繪圖指令。

為什么用這種方式?
  • 這是最基礎的繪圖方式;
  • 邏輯清晰,利于理解整個 OpenGL 渲染流程;
  • 雖然效率不高,也不適合現代項目,但非常適合教學階段使用。

如何使用 glBegin 和 glEnd

glBegin(GL_TRIANGLES)glEnd() 之間,依次調用 glVertex 來設置三角形的三個頂點,每三個點構成一個三角形:

glBegin(GL_TRIANGLES);
glVertex2f(x1, y1);
glVertex2f(x2, y2);
glVertex2f(x3, y3);
// 第二個三角形
glVertex2f(x4, y4);
glVertex2f(x5, y5);
glVertex2f(x6, y6);
glEnd();

這些坐標就是屏幕空間中我們希望繪制三角形的具體位置。


glVertex 的命名規則

OpenGL 函數的命名有一定的模式:

  • glVertex 表示定義頂點;
  • 后綴中帶的數字是維度,比如:
    • 2 表示二維(只傳 X 和 Y);
    • 3 表示三維(傳 X、Y、Z);
  • 后綴中帶的字母表示數據類型:
    • f 是 float(浮點型);
    • i 是 int(整型);
    • ub 是 unsigned byte(無符號字節);

例如:

  • glVertex2f(x, y):二維浮點坐標;
  • glVertex3i(x, y, z):三維整型坐標;
  • glVertex2ub(x, y):二維無符號字節坐標。

當前階段目標

目前我們只是要把一個由兩個三角形組成的矩形繪制出來,用來覆蓋在粉色背景上。這是驗證我們能否正確把圖形從 CPU 端發送到 GPU 并在屏幕上顯示的重要步驟。

優化等高級內容都暫時忽略,因為在這個基礎階段,核心是理解流程而不是追求極致效率。


下一步會逐步引入紋理和現代 OpenGL 的做法,但目前先專注于理解基本的圖形繪制過程。
在這里插入圖片描述

Blackboard: 用三角形覆蓋屏幕

現在我們面臨的問題是:如何確定我們要繪制的三角形的坐標?


視口(Viewport)的坐標范圍

在前面已經設置好了視口,它定義了我們在窗口中繪圖的區域范圍:

  • X 軸從 0窗口寬度
  • Y 軸從 0窗口高度

所以我們在使用頂點坐標時,應該以這個范圍為參照系,來決定三角形的具體位置。


繪制矩形所需的兩個三角形

我們要繪制的矩形,會通過兩個三角形來拼接完成,構造方式如下:

第一個三角形:
  • 左上角:(0, 0)
  • 右上角:(width, 0)
  • 右下角:(width, height)
第二個三角形:
  • 左上角:(0, 0)
  • 右下角:(width, height)
  • 左下角:(0, height)

通過這兩個三角形的拼接,完整覆蓋整個窗口區域。


坐標值來源

這些頂點坐標都是顯而易見、已知的:

  • widthheight 是當前窗口的寬度和高度;
  • 所以頂點坐標可以直接根據窗口尺寸來構造,無需復雜計算。

這一步的目標是確認:我們在不使用任何紋理的前提下,僅靠兩個三角形,能否覆蓋整個窗口區域。這是為后續貼圖打基礎的關鍵驗證步驟。

win32_game.cpp: 構建我們的第一個 OpenGL 基元,一個三角形

我們要繪制一個矩形,方法是使用兩個三角形來拼接覆蓋整個窗口。這兩個三角形的坐標非常直觀,完全基于窗口的寬度和高度來確定。


第一個三角形(下半部分)

頂點坐標如下:

  1. (0, 0) —— 左上角
  2. (window_width, 0) —— 右上角
  3. (window_width, window_height) —— 右下角

這個三角形從窗口的左上角延伸到右上角,然后再到底部右側,構成矩形的下半部分。


第二個三角形(上半部分)

頂點坐標如下:

  1. (0, 0) —— 左上角
  2. (window_width, window_height) —— 右下角
  3. (0, window_height) —— 左下角

這個三角形從左上角延伸到右下角,然后回到左下角,補上了矩形的上半部分。


總結

  • 我們利用窗口的尺寸信息 (window_width, window_height) 構造了兩個三角形;
  • 這兩個三角形拼接起來剛好完整覆蓋整個窗口區域;
  • 這個繪制方式不涉及紋理或顏色,僅僅用于測試三角形的基本繪制是否正確;
  • 其中一個被稱為下三角形(lower triangle),另一個為上三角形(upper triangle);

這是進一步將圖像貼圖到 GPU 上之前非常關鍵的一步,確認基本圖形繪制無誤。
在這里插入圖片描述

運行游戲并“看到”我們的三角形

在理論上,如果我們現在運行這段代碼,應該能夠看到兩個三角形被繪制出來。然而,實際情況是,繪制出來的兩個三角形并沒有按預期填滿整個屏幕。它們的位置并不正確。按照我們設想的坐標系統,應該是:

  • (0, 0) 是屏幕的左上角,
  • (width, 0) 是屏幕的右上角,
  • (0, height) 是屏幕的左下角,
  • (width, height) 是屏幕的右下角。

這樣,理論上這兩個三角形應該填滿整個屏幕,但實際情況是它們并沒有完全覆蓋屏幕。

問題分析:

這個問題的原因是因為 OpenGL 默認使用了固定功能管道(fixed function pipeline)。當不使用著色器時,OpenGL 會按照固定的方式進行處理。在這種情況下,坐標系沒有直接按照屏幕像素來進行映射,而是使用了不同的坐標空間,這就是為什么繪制的三角形沒有填滿整個屏幕的原因。

解決思路:

要解決這個問題,首先需要理解固定功能管道的工作原理,它會對坐標進行不同的轉換和處理,最終才會映射到屏幕上的位置。因此,為了讓三角形正確地覆蓋屏幕,需要對這些坐標進行適當的調整,或者通過使用著色器來控制坐標的轉換過程。
在這里插入圖片描述

在這里插入圖片描述

Blackboard: 固定功能管線與可編程管線

在 OpenGL 中,有兩種主要的渲染管線:固定功能管線(Fixed Function Pipeline)和可編程管線(Programmable Pipeline)。固定功能管線是早期 GPU 的工作方式,在這種模式下,GPU 只能執行一系列固定的操作,比如按某種方式處理頂點、裁剪三角形以及填充像素。而可編程管線則是現代 GPU 的工作方式,允許通過編寫著色器來實現更靈活的操作。

在固定功能管線中,最基本的頂點著色器操作已經被硬件直接實現。這個操作包括頂點變換、裁剪三角形,以及窗口空間變換(Windows space transform)。然而,在可編程管線中,很多操作都可以通過著色器自定義,頂點變換和窗口空間變換也可以通過編寫著色器來實現,而裁剪通常仍然是通過固定功能完成的。

問題出現在由于未設置合適的頂點變換,導致繪制的圖形沒有出現在預期的位置。在固定功能管線中,默認的變換方式并不會將輸入的頂點直接映射到屏幕坐標上,因此結果可能是我們無法理解的隨機位置。而如果我們自己實現著色器,可以完全控制頂點的變換和像素的填充。

理解固定功能管線的工作原理非常重要,因為我們實現的著色器實際上可以模擬固定功能管線的行為,只要設置合適的參數。
在這里插入圖片描述

在這里插入圖片描述

Blackboard: 矩陣乘法

在 OpenGL 中,矩陣和向量是核心概念。矩陣記錄了一系列數學操作,這些操作會對向量進行變換。在計算機圖形學中,矩陣乘法常用于對頂點坐標(如 3D 點)進行變換。

首先,矩陣乘法的過程是通過將矩陣的每一行與向量的每一列進行計算,生成新的向量。具體來說,當一個 3D 向量(如 (x, y, z))與一個 3x3 的矩陣相乘時,每一行的元素都會與向量的對應元素相乘,然后加和,最終得到新的坐標值。比如,假設矩陣是:

( A B C D E F G H I ) \begin{pmatrix} A & B & C \\ D & E & F \\ G & H & I \end{pmatrix} ?ADG?BEH?CFI? ?

而向量是 (x, y, z),那么矩陣與向量的乘法會按照如下步驟進行:

  • 新的 X 值是 Ax + By + Cz
  • 新的 Y 值是 Dx + Ey + Fz
  • 新的 Z 值是 Gx + Hy + Iz

這個過程就是矩陣變換。對于每個坐標軸(X、Y 和 Z),都有三個系數,分別控制 X、Y 和 Z 的輸出值。這意味著你可以根據需要調整這些系數,來得到不同的變換效果,如旋轉、縮放或平移。

理解矩陣變換非常重要,因為它是 OpenGL 渲染管線中的基礎。無論是固定功能管線還是可編程管線,矩陣和向量的變換操作都是其核心操作之一。在 OpenGL 中,矩陣變換通常用于將物體從一個坐標空間轉換到另一個坐標空間,比如從物體坐標系轉換到世界坐標系、視圖坐標系或者投影坐標系。

總的來說,矩陣是一個非常強大且靈活的工具,通過調整矩陣中的系數,能夠實現各種復雜的變換,極大地提高了圖形渲染的靈活性和效率。

舉一個簡單的例子來幫助理解矩陣和向量的變換過程。

假設有一個三維點 (x, y, z),我們想要對這個點進行 縮放旋轉平移 三種常見的變換。每種變換都可以通過矩陣乘法來實現。我們將通過具體的矩陣和向量計算來展示這些變換是如何進行的。

1. 縮放變換

縮放變換通過縮放矩陣來實現。如果我們想要將一個點的 X 軸和 Y 軸坐標分別縮放 2 倍和 3 倍,我們可以使用一個 3x3 的縮放矩陣:

s ? ( x y z ) = ( 2 0 0 0 3 0 0 0 1 ) ? ( x y z ) = ( 2 x 3 y z ) s \cdot \begin{pmatrix} x \\ y \\ z \end{pmatrix} = \begin{pmatrix} 2 & 0 & 0 \\ 0 & 3 & 0 \\ 0 & 0 & 1 \end{pmatrix} \cdot \begin{pmatrix} x \\ y \\ z \end{pmatrix} = \begin{pmatrix} 2x \\ 3y \\ z \end{pmatrix} s? ?xyz? ?= ?200?030?001? ?? ?xyz? ?= ?2x3yz? ?

然后我們將這個矩陣與點 (x, y, z) 進行矩陣乘法:

這樣,經過縮放變換后,點 (x, y, z) 會變成 (2x, 3y, z),即 X 坐標變為 2 倍,Y 坐標變為 3 倍,而 Z 坐標保持不變。

2. 旋轉變換

旋轉變換常用的旋轉矩陣是在二維或三維空間中的旋轉。例如,假設我們要在 XY 平面 上旋轉一個點 90 度(順時針旋轉)。可以使用如下的旋轉矩陣:

R = ( cos ? ( θ ) ? sin ? ( θ ) 0 sin ? ( θ ) cos ? ( θ ) 0 0 0 1 ) R = \begin{pmatrix} \cos(\theta) & -\sin(\theta) & 0 \\ \sin(\theta) & \cos(\theta) & 0 \\ 0 & 0 & 1 \end{pmatrix} R= ?cos(θ)sin(θ)0??sin(θ)cos(θ)0?001? ?

其中,θ = 90°。代入角度,我們得到:

R = ( 0 ? 1 0 1 0 0 0 0 1 ) R = \begin{pmatrix} 0 & -1 & 0 \\ 1 & 0 & 0 \\ 0 & 0 & 1 \end{pmatrix} R= ?010??100?001? ?

然后,假設我們要旋轉的點是 (x, y, z),那么將該點與旋轉矩陣相乘,得到新的坐標:

R ? ( x , y , z ) = ( 0 ? 1 0 1 0 0 0 0 1 ) ? ( x y z ) = ( ? y x z ) R \cdot (x, y, z) = \begin{pmatrix} 0 & -1 & 0 \\ 1 & 0 & 0 \\ 0 & 0 & 1 \end{pmatrix} \cdot \begin{pmatrix} x \\ y \\ z \end{pmatrix} = \begin{pmatrix} -y \\ x \\ z \end{pmatrix} R?(x,y,z)= ?010??100?001? ?? ?xyz? ?= ??yxz? ?

這樣,經過旋轉變換后,點 (x, y, z) 變成了 (-y, x, z),即在 XY 平面內順時針旋轉 90 度,Z 坐標保持不變。

3. 平移變換

平移變換通過平移矩陣實現,它是一個 4x4 矩陣,通常用于處理 3D 空間中的平移。假設我們要將點 (x, y, z) 沿 X 軸、Y 軸和 Z 軸平移一定的距離,假設分別平移 dxdydz

平移矩陣如下:

T = ( 1 0 0 d x 0 1 0 d y 0 0 1 d z 0 0 0 1 ) T = \begin{pmatrix} 1 & 0 & 0 & dx \\ 0 & 1 & 0 & dy \\ 0 & 0 & 1 & dz \\ 0 & 0 & 0 & 1 \end{pmatrix} T= ?1000?0100?0010?dxdydz1? ?

平移變換會將點 (x, y, z) 轉換為 (x + dx, y + dy, z + dz),即將點沿 X 軸平移 dx,Y 軸平移 dy,Z 軸平移 dz

綜合應用:組合縮放、旋轉和平移

我們還可以將這些變換組合起來,形成一個更復雜的變換。例如,首先進行縮放,再進行旋轉,最后進行平移。為了完成這個操作,我們可以將所有的變換矩陣相乘,然后應用到點上。

例如,假設我們先進行縮放,再旋轉,最后平移。我們可以將這些矩陣相乘,得到最終的變換矩陣:

M = T ? R ? S M = T \cdot R \cdot S M=T?R?S

然后,使用這個綜合矩陣對點進行變換。每個變換都通過矩陣乘法依次作用在點上。

總結

  • 矩陣變換 讓我們可以對三維點進行各種操作,包括縮放、旋轉和平移。
  • 通過矩陣乘法,我們能夠靈活地調整頂點的位置,并通過調整矩陣中的系數來得到不同的幾何變換效果。
  • 在 OpenGL 中,矩陣變換是渲染管線中的核心操作之一,它幫助將物體從模型空間變換到屏幕空間。

Blackboard: 齊次坐標和仿射變換

OpenGL 在處理坐標變換時,在線性變換的基礎上更進一步,引入了齊次坐標(homogeneous coordinates),從而支持更豐富的變換形式。

在線性變換中,我們通過一個矩陣與向量相乘來實現,例如:

$$
\begin{pmatrix}
a & b & c \
e & f & g \
i & j & k \
\end{pmatrix}
\cdot
\begin{pmatrix}
x \
y \
z
\end{pmatrix}

\begin{pmatrix}
ax + by + cz \
ex + fy + gz \
ix + jy + kz
\end{pmatrix}
$$

這類變換只能對輸入向量進行縮放、旋轉、錯切等操作,但無法實現平移。也就是說,無法給輸入值增加一個固定偏移量,因為矩陣中每一個值都要乘以輸入向量的某一項,沒有辦法單獨加一個固定值。例如,如果輸入是 (0, 0, 0),無論矩陣怎么寫,輸出永遠是 (0, 0, 0)

為了實現“平移”這種非線性操作,我們引入齊次坐標。通過將三維向量擴展為四維向量:

( x , y , z ) → ( x , y , z , 1 ) (x, y, z) \rightarrow (x, y, z, 1) (x,y,z)(x,y,z,1)

然后使用一個 4x4 的矩陣進行變換:

$$
\begin{pmatrix}
a & b & c & d \
e & f & g & h \
i & j & k & l \
0 & 0 & 0 & 1
\end{pmatrix}
\cdot
\begin{pmatrix}
x \
y \
z \
1
\end{pmatrix}

\begin{pmatrix}
ax + by + cz + d \
ex + fy + gz + h \
ix + jy + kz + l \
1
\end{pmatrix}
$$

其中最后一列 d, h, l 就實現了位移(偏移)功能,使得我們可以把一個物體從一個位置“搬到”另一個位置。這種帶有平移能力的變換稱為 仿射變換(affine transform),它比單純的線性變換更強大。

此外,通過控制輸入向量的第 4 個分量(即 W 分量)我們還能區分“點”和“方向”:

  • 向量 (x, y, z, 1):表示一個“點”,會受到平移的影響;
  • 向量 (x, y, z, 0):表示一個“方向”,不受平移影響,只會被旋轉或縮放。

這是因為 (x, y, z, 0) 在矩陣乘法中不會激活平移那一列,即 d, h, l 被乘以 0,等價于沒有平移。

這種區分在 3D 圖形處理中非常重要,比如:

  • 法線方向(normal vector)只需要旋轉縮放,不應被平移;
  • 頂點位置需要完整的仿射變換。

所以,總結來說:

  • 線性變換不能平移,只能旋轉、縮放、錯切;
  • 引入齊次坐標后,能夠支持平移;
  • 4x4 矩陣與 4D 向量相乘實現了仿射變換;
  • (x, y, z, 1) 是點,(x, y, z, 0) 是方向;
  • 這就是 OpenGL 中變換系統的基礎結構。

如果需要,我可以給你舉一個具體的仿射變換例子并一步步帶你算一遍。需要的話告訴我即可。

Blackboard: 模型視圖和投影矩陣

在 OpenGL 的固定功能管線中,系統定義了兩個用于頂點變換的矩陣:ModelView 矩陣Projection 矩陣。這兩個矩陣本質上是用來將我們輸入的頂點坐標從模型空間一步步轉換到最終的裁剪空間,從而使圖形可以正確地渲染在屏幕上。

這兩個矩陣雖然是分開設置的,但在實際的計算過程中,OpenGL 會將它們進行組合:將 ModelView 矩陣與 Projection 矩陣進行矩陣乘法,合并成一個最終使用的變換矩陣。這意味著,傳入的每一個頂點都會先被 ModelView 變換處理,然后再被 Projection 變換處理,最終得到裁剪空間中的坐標。

值得注意的是,矩陣乘法的順序與我們的閱讀順序相反。即如果我們寫的是:

Projection * ModelView * Vertex

那么,變換實際上是先執行 ModelView,再執行 Projection,這是因為變換是從右往左應用的。

ModelView 矩陣原本的設計中包含兩部分含義:

  • Model:將物體從局部坐標系轉換到世界坐標系;
  • View:將物體從世界坐標系轉換到攝像機(觀察)坐標系。

這兩個變換被合并為一個稱作 ModelView 的矩陣。

而 Projection 矩陣負責將場景從攝像機坐標系變換到裁剪空間,這個階段包括了視錐體變換(比如透視投影或正交投影)。

盡管歷史上為了 OpenGL 的內建光照功能而分離了這兩個矩陣(因為光照計算需要在觀察空間中完成),但在現代圖形編程中,這種分離已不再必要,尤其是當我們不使用固定功能光照時。

實際上,無論是兩個、三個還是多個矩陣,我們都可以通過矩陣乘法將它們壓縮成一個最終變換矩陣。因此,在實際編程中,我們完全可以只使用一個自定義的矩陣完成所有變換。

總結如下:

  • OpenGL 固定功能管線提供了 ModelViewProjection 兩個變換矩陣;
  • 實際頂點變換時會將它們組合成一個變換矩陣:Projection * ModelView
  • 矩陣變換是從右往左應用的,即先應用 ModelView,再應用 Projection;
  • 這兩個矩陣的分離最初是為了內建光照功能而設計的,但現在不再必要;
  • 所有的變換最終都可以組合為一個矩陣來簡化處理;
  • 我們可以忽略 ModelView,自己定義一個變換矩陣就足夠完成大部分任務。

如果需要具體的矩陣組合示例或變換流程圖,也可以繼續補充。

win32_game.cpp: 使用 glLoadIdentity 將 MODELVIEW 和 PROJECTION 矩陣設置為單位矩陣

我們在使用 OpenGL 的時候,可以完全忽略掉 ModelView 矩陣的存在,將其始終設置為單位矩陣(Identity Matrix),也就是一個什么都不做的矩陣。

OpenGL 內部實際上維護了兩個主要的變換矩陣:ModelView 矩陣Projection 矩陣。我們可以通過 glMatrixMode 來指定當前要操作的矩陣是哪一個,比如選擇 GL_MODELVIEWGL_PROJECTION。一旦選定,我們就可以對選中的矩陣進行操作。

為了讓 ModelView 矩陣不對坐標做任何變換,可以使用 glLoadIdentity()。這個函數的作用是將當前選中的矩陣重置為單位矩陣。單位矩陣的特點是,它不會對輸入的向量做任何變換,輸出等于輸入。

單位矩陣的形式如下:

1 0 0 0  
0 1 0 0  
0 0 1 0  
0 0 0 1

無論輸入的是 (x, y, z, w) 什么值,乘上這個單位矩陣后,輸出仍然是原始的 (x, y, z, w)。也就是說,它不會縮放、旋轉、平移,也不做任何形變,就是“原樣輸出”。

這種矩陣也被稱為 no-op(無操作)矩陣。它在計算中沒有實際效果,但作為初始化或占位使用非常常見。

除了單位矩陣,還可以構造 置換矩陣(Permutation Matrix),用于重新排列向量的各個分量。例如我們希望交換 x 和 y 分量,就可以構造一個對應的置換矩陣。但單位矩陣的作用就只是保持各分量原樣輸出。

在固定功能管線中,OpenGL 的行為是:

  • 它內部持有多個矩陣槽(Matrix Slot),比如:ModelView、Projection、Texture 等;
  • 我們通過 glMatrixMode 來選擇操作哪個槽;
  • 使用 glLoadIdentity() 可以清除當前槽中的變換,設置為單位矩陣;
  • 這樣可以保證該矩陣對輸入數據不施加任何變換。

通常,在 OpenGL 的默認狀態下,這些矩陣就是單位矩陣,因此頂點數據會被直接傳遞下去,沒有任何變換。我們只需要在真正想要控制變換時,才去設置這些矩陣。

總結:

  • OpenGL 內部維護多個變換矩陣(如 ModelView、Projection);
  • 可以用 glMatrixMode() 選擇要操作的矩陣;
  • 使用 glLoadIdentity() 可將選定矩陣設為單位矩陣;
  • 單位矩陣不會對輸入的頂點數據做任何處理,起到透傳作用;
  • 我們可以將 ModelView 始終設為單位矩陣,相當于它不存在;
  • Projection 矩陣則可以用于控制視圖范圍(如設置投影);
  • 這種方式簡化了變換流程,尤其適合需要完全自定義控制的場景。

這樣處理后,我們就能夠更專注于控制一個單一的 Projection 矩陣,或完全自定義我們的頂點變換邏輯。

在這里插入圖片描述

在這里插入圖片描述

win32_game.cpp: 向 glVertex2i 傳遞單位立方體,然后是 0.9f

我們可以用一個非常簡單的方法解決屏幕顯示不正確的問題,就是直接傳入處于裁剪空間(clip space)內的坐標點。所謂裁剪空間是一個標準化的單位立方體,它的坐標范圍是 [-1, 1],也就是說只要我們傳入的頂點落在這個范圍內,它們就不需要通過投影矩陣進行任何進一步的變換。這些點會直接在裁剪空間中進行裁剪處理,之后再被自動映射(歸一化設備坐標 -> 屏幕坐標)到最終的屏幕上。

例如我們將四個頂點設為 (-1, -1)、(1, -1)、(1, 1)、(-1, 1),構成一個完整填滿屏幕的矩形。這些點完全處于裁剪空間范圍內,因此不會被剔除或變形,也不會受投影矩陣的影響。最終這塊矩形會完整地填滿整個屏幕區域。

如果我們想驗證自己的理解是否正確,還可以把這些點設為稍小的值,比如設為 (-0.9, -0.9)、(0.9, -0.9)、(0.9, 0.9)、(-0.9, 0.9)。這些點仍然位于裁剪空間中,但是距離邊界略有收縮。這樣繪制出來的圖形就不會覆蓋整個屏幕,而是在屏幕中心區域內繪制一個略小的矩形。這清晰地表明了我們所傳入的坐標直接決定了圖形在屏幕上的顯示范圍。

總結要點如下:

  • 裁剪空間是 GPU 在進行可視性判斷和幾何處理時所使用的標準空間,范圍為 [-1, 1]。
  • 投影矩陣的作用是將模型坐標變換到裁剪空間,如果我們直接傳入裁剪空間的坐標,就可以跳過這一步。
  • 繪制頂點時,如果它們已經在裁剪空間內,就不再被進一步變換,最終會根據歸一化規則映射到屏幕空間。
  • 通過簡單調整坐標值大小可以直觀驗證投影和裁剪的作用。

這種方式不僅讓我們繞開了復雜的投影矩陣構建過程,而且清晰展示了裁剪空間和屏幕坐標之間的映射關系,有助于深入理解圖形渲染管線的核心機制。


問題出在哪里?

我們傳入的坐標,比如 (0, 0)(屏幕寬度, 屏幕高度),其實是“屏幕坐標”。
但 GPU 在渲染時并不是直接處理屏幕坐標的,而是先把所有點放到一個叫做 裁剪空間(Clip Space) 的地方。


什么是裁剪空間?

裁剪空間就像一個標準盒子,范圍是:

  • X:從 -11
  • Y:從 -11
  • Z:從 -11(Z 軸我們暫時可以忽略)

也就是說,只有落在這個盒子里的點才是“可見”的。
這個盒子的作用就是幫助 GPU 判斷哪些點是可渲染的,哪些該被裁掉(Clip)。


為什么我們原來畫的東西沒顯示?

因為我們傳進去的坐標,比如 (0, 0)(800, 600)超出了裁剪空間的范圍,就好像你把畫紙對準一個 1 米的窗戶,但你畫的是 10 米的大圖,窗戶外的人當然看不到完整畫面。


怎么解決?

我們直接用“標準裁剪空間”的坐標去畫,也就是:

(-1, -1) 到 (1, 1) 之間
  • (-1, -1):屏幕左下角
  • (1, 1):屏幕右上角
  • (0, 0):屏幕中心

所以我們只要傳入在這個范圍內的頂點,GPU 就知道它們在畫布的哪里了,然后再自動映射到屏幕上。


我們怎么驗證這個?

可以試著只畫中間區域,比如:

(-0.9, -0.9) 到 (0.9, 0.9)

它會畫一個比滿屏略小的矩形,位置剛好在屏幕中央。
這說明只要我們傳入的坐標在裁剪空間內,GPU 就會正確處理,顯示在我們預期的位置上。


總結(通俗版)

  • GPU不直接處理屏幕像素坐標,它先統一轉換到一個 [-1, 1] 的“標準盒子”(裁剪空間);
  • 如果坐標超出這個范圍,就會被“裁剪”掉;
  • 如果你直接傳標準范圍內的坐標(比如 -1 到 1),GPU 就會很高效地直接處理它;
  • 我們也可以自己構建一個投影矩陣去把實際坐標變成裁剪坐標,但現在先跳過,直接用裁剪坐標最簡單;

如果你希望我畫一張圖來輔助解釋這個裁剪空間和屏幕坐標的關系,我可以幫你做一個可視化圖示,會更直觀!要不要?
在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

運行游戲并查看更小的矩形

我們將繪制區域在寬度和高度方向上分別縮小了10%,也就是說我們傳入的頂點坐標從原來的 (-1, -1)(1, 1) 縮小到了 (-0.9, -0.9)(0.9, 0.9)。這相當于在裁剪空間里,矩形的每一邊都向內收縮了10%。

需要注意的是,這個“10%”是相對于裁剪空間中的坐標范圍 [-1, 1] 來說的,而不是相對于屏幕像素的絕對大小。這就意味著當最終從裁剪空間映射到屏幕空間時,X 軸和 Y 軸的縮放比例不同,因為屏幕的寬和高并不是相等的。

例如,如果屏幕是寬屏(例如 16:9),那么 X 軸的放大倍數會比 Y 軸更大,因此相同“10%”的縮小在視覺上會讓上下邊緣的留白看起來比左右邊緣更小。這并不是因為我們設置的值不對,而是由于屏幕在水平方向上的拉伸更大,導致相對單位長度在屏幕上的顯示寬度更長。

也就是說:

  • 在裁剪空間中我們以相同的比例(比如0.9)縮小了 X 和 Y;
  • 但當映射到屏幕時,由于屏幕的長寬比不同,這個縮放在屏幕上表現為 寬邊留白更多,高邊留白更少
  • 這是裁剪空間的坐標變換到屏幕坐標時根據長寬比進行非等比縮放的自然結果。

這個過程是固定定義好的,圖形渲染管線就是這樣工作的。因此只要我們理解了裁剪空間的原理以及它是如何被最終映射到屏幕上的,就能夠很好地預測圖形在屏幕上最終的顯示效果。

總結一下要點:

  • 縮小10%是指在裁剪空間內縮小,也就是從 ±1 縮小到 ±0.9
  • 由于裁剪空間會映射到實際屏幕,而屏幕寬高比不同,導致縮小效果在水平和垂直方向上看起來不一樣;
  • 橫向放大更多 → 相對留白更寬;
  • 這是裁剪空間到屏幕空間映射中不可避免的比例失衡;
  • 這種行為是圖形管線的設計規范,理解之后就能更精準地控制顯示區域。

如果你希望,我還可以畫圖幫你可視化一下這種從裁剪空間到屏幕空間映射的過程,會更直觀。需要嗎?

Blackboard: 從裁剪空間到屏幕空間的轉換

我們之所以看到圖形縮放后的位置和大小變化,是因為 OpenGL 的坐標變換過程是**先在裁剪空間中進行處理,然后再映射到屏幕空間(screen space)**的。這種流程是 OpenGL 的規定行為,不是我們能夠改變的。

具體來說:


一、從裁剪空間到屏幕空間的過程

我們在裁剪空間中繪制的坐標范圍是 [-1, 1],這個范圍代表的是一個“單位立方體”(unit cube),中心是 (0, 0),左下角是 (-1, -1),右上角是 (1, 1)。

而屏幕空間的坐標是以像素為單位的,比如一個 1920x1080 的屏幕,其左下角是 (0, 0),右上角是 (1920, 1080)。我們需要把裁剪空間中的點,轉換成這個屏幕空間中的點。

這個變換過程分兩步:


二、第一步:將裁剪空間移動到從 [0, 0][2, 2]

我們的裁剪空間是 [-1, 1],也就是說,它的寬度是 2。我們要把它的最小值 -1 移動到 0,那只需要加一個偏移量 (1, 1)

舉個例子,原點 (0, 0) 加上 (1, 1) 變成了 (1, 1),左下角 (-1, -1) 加上 (1, 1) 變成 (0, 0),右上角 (1, 1) 加上 (1, 1) 變成 (2, 2)。這一步是把整個裁剪空間從 [-1, 1] 移動到了 [0, 2]


三、第二步:縮放到屏幕像素大小

現在裁剪空間的坐標是 [0, 2],我們想把它映射到屏幕像素空間,比如 [0, 1920][0, 1080]。很簡單,我們乘以寬度的一半(width/2)和高度的一半(height/2)

舉例:

最終屏幕坐標 = (裁剪坐標 + 1) × (寬度 / 2, 高度 / 2)

舉個具體例子,假設我們有一個點 (0.5, -0.5),在 1920×1080 屏幕上:

x' = (0.5 + 1) × (1920 / 2) = 1.5 × 960 = 1440
y' = (-0.5 + 1) × (1080 / 2) = 0.5 × 540 = 270

這個點在屏幕上的坐標就是 (1440, 270)


四、屏幕映射變換由誰控制?

雖然我們不會手動去做這些加法和乘法,但是 OpenGL 會自動做這些變換,它是通過我們設置的 glViewport() 函數來獲得屏幕尺寸的。這個函數告訴 OpenGL:

  • 最左邊的起點是哪里(x, y)
  • 這個視口的寬度和高度是多少(w, h)

于是 OpenGL 就會用這些信息來計算坐標映射規則。

我們可以設置 viewport 的參數來控制這個“映射關系”,比如我們可以讓視口不是從 (0, 0) 開始,也不是覆蓋整個窗口,而是只映射到一個小區域,從而讓圖形繪制在屏幕某個角落。我們雖然現在是默認讓它對齊整個屏幕,但完全可以讓它繪制到別處。


總結核心邏輯:

  • OpenGL 總是先在裁剪空間中工作(范圍 [-1, 1]);
  • 然后會通過加偏移和乘縮放因子,把點映射到屏幕空間(單位像素);
  • 加偏移是 (x + 1, y + 1),變成 [0, 2]
  • 乘縮放是 × (width/2, height/2),變成屏幕坐標;
  • 這個變換是固定邏輯,不是魔法,就是簡單的數學;
  • 我們唯一能控制的是 glViewport 的設置,它提供 width 和 height;
  • 所以 glViewport 決定了最終的“屏幕顯示映射”區域。

如果你還希望可視化這個變換流程,我可以畫一張圖來更直觀地展示整個過程。你想要我畫圖嗎?

Blackboard: 繪制紋理

我們已經成功在屏幕上畫出了一個矩形,意味著基礎的圖形渲染流程已經打通了。接下來我們要做的,就是讓這個矩形顯示出一張紋理圖像,也就是把圖像“貼”到這個矩形上。


一、為什么需要 UV 坐標?

為了讓 GPU 知道該在圖像的哪個部分采樣顏色信息,我們需要給矩形的每個頂點附加一組 UV 坐標
UV 坐標是一個二維坐標系,用于在紋理圖像中標識位置,取值范圍是 [0, 1]

  • (0, 0) 表示紋理圖的左下角;
  • (1, 1) 表示右上角;
  • 中間的坐標表示圖像上的其他任意位置。

我們可以把一張圖片看作一個“UV 平面”,通過指定頂點的 UV 坐標,就能告訴渲染管線在繪制這個矩形時,該從圖片的哪個位置采樣顏色并貼到屏幕上


二、怎么設置 UV 坐標?

我們要繪制一個矩形,那么它有四個頂點。對于每個頂點,我們除了傳入它在裁剪空間中的位置(clip space 坐標),還要傳入對應的 UV 坐標:

頂點位置(clip)對應 UV 坐標
(-1, -1)(0, 0)
(1, -1)(1, 0)
(1, 1)(1, 1)
(-1, 1)(0, 1)

這樣,當 GPU 在三角形內部進行插值計算時,就會自動為每個像素計算一個對應的 UV 值,從而能夠正確從紋理圖像上取到相應的顏色進行渲染。


三、采樣紋理的過程

GPU 拿到每個片元的 UV 坐標后,會:

  1. 根據 UV 坐標在綁定的紋理圖中找到對應的像素;
  2. 從紋理圖中取出該像素顏色;
  3. 把這個顏色用于當前的片元(像素)渲染。

這個過程就是所謂的“紋理采樣”。


四、這個流程和我們之前手動實現的很像

我們之前自己模擬實現過一個圖像采樣過程,比如直接從圖像數組中按照 (x / width, y / height) 的方式取顏色,概念上其實和現在在 GPU 上做的幾乎是一樣的:

  • 都是在一個二維空間中,用一組歸一化的坐標 [0,1] 表示采樣點;
  • 都需要根據這些坐標映射到原始圖片上的實際像素;
  • 都要插值或采樣出顏色值,作為最終顯示的像素顏色。

現在我們只是把這些交給了 GPU 自動執行,效率更高、控制力更強。


總結

  • 我們在屏幕上繪制矩形后,為了貼圖,需要給每個頂點設置 UV 坐標;
  • UV 坐標表示紋理圖像中的位置,范圍為 [0, 1];
  • 頂點位置 + UV 坐標共同決定了圖形形狀和紋理采樣方式;
  • 片元著色器會用插值后的 UV 坐標從紋理中采樣顏色;
  • 這個采樣過程和我們手動實現過的紋理映射邏輯非常類似。

接下來只要我們綁定一張紋理圖,然后讓著色器根據傳入的 UV 坐標進行采樣,就可以實現在矩形上顯示圖像的效果了。想繼續講紋理綁定和片元著色器的細節嗎?

win32_game.cpp: 在兩個三角形之前執行 glColor3f

我們現在要講的是 OpenGL 的一種傳統模式,也就是舊式固定功能管線的工作方式。在這種模式下,每次調用 glVertex 都會被認為是“提交”一個頂點,而在這之前所設置的各種狀態(比如顏色、紋理坐標等)會自動關聯到這個頂點上。


一、頂點屬性的綁定方式

在固定功能管線中,OpenGL 使用一種“順序聲明”的方式來綁定屬性:

  • 調用 glColor3f 設置顏色;
  • 調用 glTexCoord2f 設置紋理坐標;
  • 然后調用 glVertex3f 設置頂點位置。

這些屬性都會自動綁定到當前這個頂點上。
一旦 glVertex 被調用,OpenGL 就會把之前設置的所有屬性值與該頂點綁定。下一次設置屬性的時候,就會為下一個頂點準備。


二、示例說明:顏色插值

假設我們繪制一個矩形的兩個頂點:

  • 第一個頂點之前設置顏色為黃色
  • 第二個頂點之前設置顏色為白色

那么最終這兩個頂點之間的區域,OpenGL 會自動做顏色插值——也就是說,在圖形片元之間會自動混合顏色,讓顏色從黃色平滑過渡到白色。

類似地,我們也可以為每個頂點分配不同的顏色:

  • 第一個頂點設置為紅色;
  • 第二個設置為綠色;
  • 第三個設置為藍色。

那么渲染出來的圖形在三個頂點之間會自動生成一個紅-綠-藍之間漸變過渡的彩色區域。

這個行為和我們之前講過的 UV 坐標插值非常類似 —— 頂點的任何屬性(顏色、紋理坐標等)都會在片元階段自動插值,這正是現代圖形渲染中實現豐富視覺效果的基礎。


三、紋理坐標的指定

既然我們已經了解了顏色是如何被關聯到頂點上的,那么紋理坐標(UV 坐標)也是一樣:

  • 我們調用 glTexCoord2f(u, v) 設置某個頂點的紋理坐標;
  • 然后調用 glVertex3f(x, y, z) 設置該頂點的位置;
  • 此時這個頂點就擁有了兩個屬性:一個是位置坐標,另一個是 UV 坐標。

OpenGL 會在片元階段自動插值這些 UV 坐標,然后根據插值結果從紋理圖像中采樣顏色進行著色。


四、小結與思路

  • 在舊式 OpenGL 中,頂點的屬性設置(顏色、紋理坐標等)是在 glVertex 之前調用的;
  • 每個頂點可以擁有自己的顏色、紋理坐標等屬性;
  • OpenGL 會自動在多個頂點之間進行插值,生成平滑的顏色或紋理過渡效果;
  • 我們只需調用合適的設置函數(如 glColor3fglTexCoord2f),并正確地和頂點一一對應;
  • 整個機制本質上是一種狀態機式的提交方式,先設置屬性、再提交頂點。

這種方式雖然比較老派,但它清楚地體現了圖形渲染中“屬性 -> 插值 -> 繪制”這個基本思想,對于理解現代著色器系統也非常有幫助。接下來我們會在實際代碼或圖板中手動列出每個頂點對應的紋理坐標,進一步實現完整的紋理貼圖效果。

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

Blackboard: 建立我們的 u,v 紋理坐標

我們在貼圖時,需要為每個頂點指定正確的紋理坐標(UV 坐標),才能確保紋理圖像準確映射到我們繪制的圖形上。現在我們假設已經繪制了一個矩形(由兩個三角形構成),目標是將一張完整的圖片平鋪覆蓋在這個矩形上。


一、明確貼圖目的

我們希望紋理完整而準確地貼合在矩形上,也就是說:

  • 圖片的左下角對齊矩形的左下角;
  • 圖片的右上角對齊矩形的右上角;
  • 中間的部分也一一對應。

因此,我們需要為每個頂點提供與其幾何位置對應的紋理坐標(UV 坐標)。紋理坐標的范圍是 [0, 1],其中:

  • U = 0 表示紋理圖像的最左邊;
  • U = 1 表示紋理圖像的最右邊;
  • V = 0 表示紋理圖像的最下邊;
  • V = 1 表示紋理圖像的最上邊。

二、具體頂點與紋理坐標的匹配關系

假設我們繪制的是一個矩形,由兩個三角形拼成:

三角形1:左下 -> 右下 -> 右上  
三角形2:左下 -> 右上 -> 左上

那么我們為每個頂點分配如下 UV 坐標:

頂點位置對應紋理坐標(UV)
左下角(0, 0)
右下角(1, 0)
右上角(1, 1)
左上角(0, 1)

通過這樣的紋理坐標設置,可以確保整個紋理圖像在屏幕上的矩形區域中被完整顯示,不會出現錯位或拉伸。


三、小結與原理

  • UV 坐標控制紋理圖像如何貼在幾何圖形表面;
  • UV 坐標范圍 [0, 1] 對應整個紋理圖像的寬高;
  • 在屏幕上繪制一個矩形時,只需要按對應關系給四個角分別設定 (0,0)、(1,0)、(1,1)、(0,1);
  • OpenGL 會在三角形內插值這些紋理坐標,再據此從紋理圖像中采樣顏色;
  • 這樣就能讓圖像準確鋪在矩形表面。

通過合理設置 UV,我們實現了一個簡單的全屏貼圖操作,是圖像渲染中的基礎步驟。
在這里插入圖片描述

win32_game.cpp: 設置我們的紋理坐標

我們現在已經為所有用于繪制矩形的頂點分配了正確的紋理坐標(UV 坐標),這一步非常關鍵,它確保紋理圖像能正確映射到我們繪制的圖形上。


一、完整的紋理坐標分配

我們所繪制的是一個矩形,由兩個三角形構成:

  • 第一個三角形是從左下角 → 右下角 → 右上角;
  • 第二個三角形是從左下角 → 右上角 → 左上角。

對于這些頂點,我們為其分配了以下紋理坐標:

頂點位置對應紋理坐標(UV)
左下角 (?P,?P)(0, 0)
右下角 (P,?P)(1, 0)
右上角 (P,P)(1, 1)
左上角 (?P,P)(0, 1)

這種分配方式確保紋理圖像完整地貼合整個矩形區域。UV 的值清晰地描述了紋理圖像中對應的區域如何貼合到屏幕上的幾何形狀。


二、當前執行效果說明

盡管我們已經給所有頂點都分配好了紋理坐標,但此時運行程序時并不會看到任何圖像上的變化,原因很簡單:我們尚未提供紋理本身的圖像數據,也就是沒有指定實際的紋理內容。

此時的狀態是:

  • 幾何圖形已經準備好;
  • UV 坐標已經綁定好;
  • 但紋理圖像數據為空,因此渲染結果看起來沒有變化。

三、紋理矩陣與變換(可選知識)

除了綁定靜態的紋理坐標,還可以使用紋理矩陣(texture matrix)來對紋理進行額外的變換。這允許我們實現一些更高級的效果,例如:

  • 滾動紋理(平移 UV);
  • 縮放或旋轉紋理;
  • 鏡像翻轉紋理;
  • 動態動畫效果等。

雖然我們在這里不會實際使用它,但可以通過設置 GL_TEXTURE 模式并應用矩陣變換,對紋理坐標進行進一步處理,從而改變紋理在圖形上的映射方式。這種變換只影響紋理坐標,而不影響幾何圖形的位置。


四、接下來要做的事

下一步是把實際的紋理圖像數據“上傳”到顯卡(即 GPU),讓 OpenGL 使用這張紋理來進行采樣和渲染。

不過這一步在實際操作中并不簡單,因為:

  • OpenGL 的紋理上傳過程涉及多個狀態和函數調用;
  • 默認狀態下可能存在兼容性問題或格式不正確;
  • 需要正確設置紋理參數(如過濾模式、環繞模式等);
  • 還需要保證數據格式與 GPU 所期望的格式一致。

因為時間關系,我們暫時不會在這一節課上完成上傳操作,而是先寫下相關函數調用框架,作為下次詳細講解的基礎。


總結

  • 已完成頂點坐標與 UV 坐標的綁定;
  • 正確分配紋理坐標后,圖像可以完整映射到矩形;
  • 雖未加載紋理數據,但坐標已經就緒;
  • 可選使用紋理矩陣進行進一步變換;
  • 下一步是加載并上傳紋理圖像數據,使紋理真正顯示在屏幕上。

目前我們已經完成了紋理映射的準備工作,接下來只需將圖像傳遞到顯卡,即可完成完整的紋理繪制流程。
在這里插入圖片描述

在這里插入圖片描述

win32_game.cpp: 使用 glTexImage2D 向圖形卡提交紋理

在OpenGL中,指定紋理的過程是通過調用 glTexImage2D 來實現的,這個函數允許我們為紋理提供圖像數據,并指定如何存儲和使用這些數據。不過,這個過程比較繁瑣,涉及到一些非常復雜且難以理解的參數,因此可以說這是OpenGL中一些最糟糕的設計之一。


一、glTexImage2D函數的參數說明

  • 像素數據glTexImage2D 最后的一個參數是 pixels,它是指向圖像像素數據的指針。這一部分的處理方式與我們之前創建緩沖區的方式類似,因此可以理解為它指定了一個圖像的像素數據。

  • 寬度和高度:顯而易見,這是圖像的尺寸,分別表示圖像的寬度和高度。

  • 邊框border 參數定義了圖像是否有額外的邊框環繞它。在大多數情況下,我們并不使用邊框,因此它通常設置為 0。

  • 格式參數:參數 internalFormatformat 定義了如何在內存中存儲紋理數據以及我們如何提供數據給OpenGL。

    • format 是指我們傳入的數據的格式,比如 RGB 或 RGBA。
    • internalFormat 是OpenGL如何存儲這些數據的建議格式。例如,在RGB情況下,我們使用 GL_RGBA8 來指定每個顏色通道使用 8 位存儲。
  • 顏色順序(BGR與RGB):因為Windows平臺使用的是BGR格式(而非常見的RGB),所以內存中的顏色順序是 BGR 而不是 RGB。在OpenGL中,處理這種格式是正常的,可以通過 GL_BGRGL_BGRA 來指定。

  • 類型type 參數定義了每個顏色分量的數據類型。在這種情況下,我們通常使用 GL_UNSIGNED_BYTE,表示每個顏色通道是一個無符號字節(0到255)。

  • 目標類型target 參數指定了紋理的類型,它告訴OpenGL我們正在處理的是1D紋理、2D紋理還是其他類型的紋理。

  • 級別level 參數通常用于指定多級漸遠紋理(Mipmaps),這里我們設置為0,因為我們不使用多級漸遠紋理。


二、如何上傳紋理到顯卡

上傳紋理到顯卡并不簡單。glTexImage2D 會將像素數據傳輸到顯卡內存中,但是OpenGL并不會立刻執行這個操作。相反,它會將此操作放入命令隊列,并稍后執行。這也意味著,我們調用了 glTexImage2D 后,紋理可能并不會立即顯示在屏幕上。


在這里插入圖片描述

三、啟用紋理功能

為了使紋理生效,我們需要明確啟用紋理功能。OpenGL的固定功能管線允許我們啟用或禁用一些特性,紋理也是其中之一。我們需要通過 glEnable(GL_TEXTURE_2D) 來啟用紋理操作。

然而,僅僅啟用紋理并不意味著我們能夠看到紋理。因為OpenGL會保持一些狀態,而我們需要確保在合適的時機綁定正確的紋理。


在這里插入圖片描述

四、綁定紋理

OpenGL允許我們管理多個紋理,因此我們必須明確地綁定紋理。每個紋理都有一個“槽”,我們需要用 glBindTexture 來綁定正確的紋理槽。

  • 生成紋理句柄:我們需要通過 glGenTextures 函數生成紋理句柄。這個句柄就像一個指針,它幫助我們在OpenGL內部標識紋理。

  • 綁定紋理:綁定紋理意味著告訴OpenGL“接下來的操作會應用到哪個紋理”。這個綁定過程確保了每次繪制時,正確的紋理被應用到目標圖形。


在這里插入圖片描述

五、設置紋理環境

紋理本身并不會直接影響圖形的顏色,它只是提供了一個額外的信息源,用來影響像素的最終顏色。我們需要配置紋理環境,以定義紋理如何與其他顏色信息結合。

OpenGL提供了一個 glTexEnv 函數來控制紋理與顏色的混合模式。最常用的模式之一是 GL_MODULATE,這表示紋理的顏色會與當前顏色相乘,從而影響最終繪制的顏色。


在這里插入圖片描述

glTexEnv 是 OpenGL 固定管線中用于設置 紋理環境(Texture Environment) 的函數,作用是告訴 OpenGL:紋理顏色和當前顏色(如 glColor 設置的顏色)如何組合,這一步是在片元著色前決定最終像素顏色的重要一環。


函數原型

void glTexEnvf(GLenum target, GLenum pname, GLfloat param);
void glTexEnvi(GLenum target, GLenum pname, GLint param);

常用參數解析

參數含義
target一般為 GL_TEXTURE_ENV,表示修改紋理環境參數
pname指定要設置的屬性,通常用 GL_TEXTURE_ENV_MODE
param設置值(模式),如 GL_MODULATEGL_REPLACEGL_DECALGL_BLEND

常見模式解釋

模式效果說明
GL_REPLACE使用紋理顏色替代原始顏色結果顏色只取紋理顏色,忽略 glColor 設置的顏色
GL_MODULATE紋理顏色 * 當前顏色最常用,用于根據頂點顏色對紋理做亮度調節
GL_DECAL僅貼圖 RGB,忽略原色類似于 REPLACE,但更適用于不透明紋理
GL_BLEND紋理顏色和當前顏色混合按指定混合方式融合,較少使用

示例代碼

// 設置紋理顏色與頂點顏色進行相乘(常用)
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);// 替換為紋理顏色(不受 glColor 影響)
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);

使用流程中的位置

調用 glTexEnv 通常出現在設置紋理前或綁定紋理后,如:

glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, texture_id);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); // 設置紋理混合模式

可視化理解(假設紋理顏色為紅色,glColor 設置為綠色):

  • GL_REPLACE:最終顏色 = 紅色(紋理)
  • GL_MODULATE:最終顏色 = 紅色 × 綠色 = 黑色(因為 r×0, g×1, b×0)
  • GL_DECAL:最終顏色 = 紅色(忽略 glColor)

注意事項

  • 這個函數 僅適用于固定管線(OpenGL 1.x),在使用著色器(OpenGL 3.3+ 或現代 OpenGL)時已被廢棄。
  • GL_TEXTURE_ENV_MODE 是設置整體行為,和 glTexParameteri 設置采樣規則不同。

總結一句話

glTexEnv 決定了 紋理顏色如何與已有顏色(如頂點色)混合,是固定管線渲染中控制最終色彩表現的關鍵控制點。


六、總結

  1. 上傳紋理:通過 glTexImage2D 上傳圖像數據,指定圖像的格式、大小、顏色等參數。
  2. 啟用紋理:調用 glEnable(GL_TEXTURE_2D) 來啟用紋理功能。
  3. 綁定紋理:使用 glBindTexture 綁定正確的紋理,以確保后續的繪制操作應用到正確的紋理。
  4. 設置紋理環境:通過 glTexEnv 設置紋理與其他顏色的結合方式,例如使用 GL_MODULATE 模式將紋理顏色與當前顏色相乘。

盡管我們已經完成了紋理的上傳與綁定工作,但最終的效果仍然未必顯示出來。這是因為OpenGL的操作是非常狀態驅動的,我們需要進一步完善紋理采樣規則,確保紋理正確應用到圖形上。
在這里插入圖片描述

glTexImage2D 是 OpenGL 中用于 指定一個二維紋理圖像 的核心函數,它的作用是將我們準備好的圖像數據上傳給 GPU,以便在渲染時能通過紋理采樣使用這些圖像。它是紋理工作的關鍵步驟之一。


函數原型

void glTexImage2D(GLenum target,GLint level,GLint internalFormat,GLsizei width,GLsizei height,GLint border,GLenum format,GLenum type,const void * data
);

每個參數詳細解釋

參數名含義
target紋理目標,通常是 GL_TEXTURE_2D(表示二維紋理)
levelMipmap 級別,0 表示基礎級,越大越小分辨率,初期一般為 0
internalFormat指定 GPU 存儲紋理的格式,如 GL_RGBA8 表示 8 位每通道 RGBA
width / height圖像的寬度和高度
border是否有邊框,必須是 0(OpenGL ES 已廢棄此參數)
format數據格式,如 GL_BGRA_EXT, GL_BGR_EXT,描述傳入的數據通道順序
type每個通道的數據類型,如 GL_UNSIGNED_BYTE(8 位無符號整數)
data實際圖像像素數據的指針,可以是 unsigned char* 等類型

舉個例子

glTexImage2D(GL_TEXTURE_2D,       // target0,                   // level (base level)GL_RGBA8,            // internal format (store as 8-bit per channel RGBA)buffer_width,        // widthbuffer_height,       // height0,                   // border (must be 0)GL_BGRA_EXT,             // format (how the data is laid out)GL_UNSIGNED_BYTE,    // type (each channel is 8-bit)buffer_memory        // pointer to pixel data
);

使用前置條件(調用前必須做的事情)

在調用 glTexImage2D 前,必須完成以下步驟:

  1. 生成紋理 ID

    GLuint texture_id;
    glGenTextures(1, &texture_id);
    
  2. 綁定紋理

    glBindTexture(GL_TEXTURE_2D, texture_id);
    
  3. 設置紋理參數(如過濾模式、環繞方式)

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    

然后再調用 glTexImage2D 上傳圖像。


常見問題與坑

問題說明
圖像顯示不出來沒有啟用紋理,或者沒有正確綁定紋理、設置參數
顏色錯亂format 與內存中的通道順序不匹配(如 BGRA vs RGBA)
崩潰或花屏data 指針錯誤,或者圖像尺寸不合法(寬高為0)
圖像模糊可能沒有設置 GL_TEXTURE_MIN_FILTERGL_TEXTURE_MAG_FILTER

總結一句話:

glTexImage2D 是把你CPU準備的圖像數據上傳到GPU顯存中,給OpenGL渲染時使用的關鍵一步,只有數據、格式、尺寸、綁定、參數都設置對了,圖像才會被正確顯示出來。


在這里插入圖片描述

win32_game.cpp: 使用 glTexEnvi 和 glTexParameteri

我們繼續講解 OpenGL 中紋理相關的設置流程。


在我們啟用紋理之后,僅僅使用 glTexEnv 設置紋理的混合方式(如 GL_MODULATE,實現紋理顏色與頂點顏色的相乘)還不夠,我們還需要進一步配置紋理的參數,這時候就要使用 glTexParameter


glTexParameter 概述

glTexParameter 是一個非常核心的函數,它用于設置紋理對象的各種行為,比如:

  • 紋理的采樣方式(放大/縮小時使用何種算法)
  • 是否啟用紋理重復(環繞)或邊緣拉伸
  • mipmapping 行為(如果啟用)
  • 邊界顏色(當超出紋理范圍時)

函數原型

void glTexParameteri(GLenum target, GLenum pname, GLint param);
void glTexParameterf(GLenum target, GLenum pname, GLfloat param);

參數說明

  • target:通常為 GL_TEXTURE_2D,表示這是一個二維紋理的設置
  • pname:設置項的名字,比如 GL_TEXTURE_MIN_FILTERGL_TEXTURE_WRAP_S
  • param:具體的參數值,比如 GL_NEARESTGL_LINEARGL_REPEAT

常用設置項舉例

設置項(pname)含義常用取值
GL_TEXTURE_MIN_FILTER縮小紋理時的采樣方式GL_NEAREST, GL_LINEAR
GL_TEXTURE_MAG_FILTER放大紋理時的采樣方式GL_NEAREST, GL_LINEAR
GL_TEXTURE_WRAP_S水平方向超出范圍時行為GL_REPEAT, GL_CLAMP
GL_TEXTURE_WRAP_T垂直方向超出范圍時行為GL_REPEAT, GL_CLAMP

示例設置代碼

// 設置放大和縮小時都使用線性過濾
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);// 設置紋理在超出坐標范圍時重復顯示
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

總結一下我們到目前為止做的內容:

  1. 使用 glTexEnv 設置紋理與當前顏色的混合方式(例如 GL_MODULATE 代表相乘)
  2. 使用 glTexParameteri 設置紋理的采樣方式與邊界行為等
  3. 這些設置都依賴于 glBindTexture 綁定的當前紋理對象

注意

即使完成了上面的所有設置,有時畫面上仍然不會出現任何內容。這很可能是因為還有一些其他問題(比如沒有正確傳入紋理坐標,或者片元根本沒有被繪制),這個問題可以留作后續調試練習。


https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glTexParameter.xhtml

在這里插入圖片描述

注意:我們還沒有指定步幅(stride)

在使用 glTexImage2D 上傳紋理數據時,即使已經正確設置了寬度、高度、顏色格式(如 GL_RGBA)、數據類型(如 GL_UNSIGNED_BYTE)等參數,仍然可能遇到紋理無法正確顯示的問題。一個常被忽視的關鍵點是**像素數據的行間距(stride)和數據排列方式(packing)**沒有被顯式指定,這可能導致紋理數據在上傳時出現錯位或扭曲。


主要問題:缺少像素對齊方式的設置

默認情況下,OpenGL 會根據其內部規則來推測圖像每一行的數據間距(如是否按照4字節對齊)。如果上傳的數據在內存中的排列方式與 OpenGL 期望的不一致,就會出現圖像顯示異常的問題。


什么是 stride 和 packing

  • stride(步長):圖像中每一行占據的字節數,可能大于圖像寬度 × 每像素字節數。舉例來說,某些圖像數據格式可能在每行后填充額外的字節用于對齊。

  • packing(數據對齊方式):OpenGL 如何解析 CPU 提供的圖像數據的每一行之間的間距。默認是按照4字節對齊的,即每一行數據長度必須是4的倍數;如果不是,就可能需要手動指定。


如何解決:使用像素存儲參數函數

OpenGL 提供了幾個函數用于設置像素存儲的方式,最常用的是:

glPixelStorei(GL_UNPACK_ALIGNMENT, x);

這個函數設置 CPU 到 GPU 傳輸像素數據時的行對齊方式

  • x 可以是 1, 2, 4, 或 8(默認是 4)
  • 如果圖像寬度乘每像素字節數不是 4 的倍數,應該設置為 1

示例

假設我們上傳的圖像數據是一張 3x3 的 RGB 圖像(每像素3字節),3×3 = 9 字節每行,這不是4的倍數,所以必須這樣寫:

glPixelStorei(GL_UNPACK_ALIGNMENT, 1);  // 按字節對齊
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 3, 3, 0, GL_RGB, GL_UNSIGNED_BYTE, imageData);

否則,OpenGL 會認為每行是4字節對齊,讀取時會跳過錯誤的字節,導致圖像錯亂。


還有哪些相關函數

除了 GL_UNPACK_ALIGNMENT,還有其他像素傳輸設置函數可以根據需要配置:

函數說明
glPixelStorei(GL_UNPACK_ROW_LENGTH, value)每一行像素的長度(單位:像素)
glPixelStorei(GL_UNPACK_SKIP_ROWS, value)跳過圖像前 value
glPixelStorei(GL_UNPACK_SKIP_PIXELS, value)每行開頭跳過 value 個像素
glPixelStorei(GL_UNPACK_ALIGNMENT, value)行對齊方式,默認是 4

這些設置用于告訴 OpenGL:從 CPU 提供的內存中解析圖像數據時該如何跳過、對齊和讀取像素。


總結

  1. 使用 glTexImage2D 上傳紋理時,不僅要提供尺寸、格式、數據類型等信息,還必須確保數據的行對齊方式與 OpenGL 設置一致。
  2. 若圖像數據不是4字節對齊,必須調用 glPixelStorei(GL_UNPACK_ALIGNMENT, 1) 等函數手動指定正確的 unpack 設置。
  3. 如果紋理出現錯亂、錯位或顏色異常,第一時間應檢查像素傳輸參數設置是否與實際數據格式一致。

是否還想補充 glTexSubImage2D 的使用方法或紋理更新相關內容?

win32_game.cpp: 解釋這些參數

在設置紋理參數時,有許多關鍵項需要理解和配置。我們將按類別詳細梳理這些設置的意義和使用方法,以下是對 glTexParameter 中常見配置項的全面解析:


紋理過濾設置(Texture Filtering)

1. GL_TEXTURE_MIN_FILTER

  • 作用:控制紋理在被縮小時(即原始紋理分辨率大于實際渲染顯示區域)如何采樣。
  • 常用設置
    • GL_NEAREST:使用最鄰近的像素,不做任何插值(不平滑)。
    • GL_LINEAR:使用雙線性插值,進行模糊處理。
    • 其他如 GL_NEAREST_MIPMAP_NEAREST 等用于 mipmapping,但我們不使用。

我們的配置
我們關閉所有 mipmapping,選擇 GL_NEAREST,這樣當紋理縮小時直接采樣最近的 texel,不進行插值或模糊處理。

2. GL_TEXTURE_MAG_FILTER

  • 作用:控制紋理在被放大時(紋理分辨率小于實際渲染顯示區域)如何采樣。
  • 常用設置
    • GL_NEAREST:直接放大最近的像素,效果類似像素風格。
    • GL_LINEAR:放大時做插值處理,更平滑。

我們的配置
同樣設置為 GL_NEAREST,關閉任何模糊效果,方便調試和觀察原始像素數據。


Mipmapping 相關參數(我們不使用)

Mipmapping 是一種預處理技術,用于生成不同分辨率的紋理版本以提升縮放時的渲染性能和質量。但我們暫時禁用該特性,因此以下參數全部忽略:

  • GL_TEXTURE_BASE_LEVEL / GL_TEXTURE_MAX_LEVEL
  • GL_TEXTURE_MIN_LOD / GL_TEXTURE_MAX_LOD

紋理坐標環繞模式(Texture Wrapping)

OpenGL 使用 S, T, R 來表示紋理坐標軸:

  • S 對應 U 坐標(橫向)
  • T 對應 V 坐標(縱向)
  • R 對應 W 坐標(用于三維紋理)

常用設置項:

  • GL_TEXTURE_WRAP_S:S軸的環繞方式
  • GL_TEXTURE_WRAP_T:T軸的環繞方式
  • (如果是三維紋理,還可設置 GL_TEXTURE_WRAP_R

可選值:

  • GL_CLAMP_TO_EDGE:坐標超出[0,1]范圍后鉗制到邊緣像素
  • GL_REPEAT:坐標超出[0,1]范圍后重復紋理
  • GL_MIRRORED_REPEAT:坐標超出后以鏡像方式重復
  • GL_CLAMP_TO_BORDER:超出時使用邊界顏色(需要設置邊界色)

我們的配置
我們選擇 GL_CLAMP_TO_EDGE,不希望出現紋理重復或鏡像,僅希望坐標超出范圍后直接貼邊。


其他不常用參數(我們忽略)

  • GL_TEXTURE_PRIORITY:設定紋理在 GPU 顯存中的優先級,用于內存管理,我們暫不涉及。
  • GL_TEXTURE_RESIDENTGL_TEXTURE_COMPARE_MODE 等也不適用于當前上下文。

注意版本兼容性

有些參數或功能在當前使用的 OpenGL 版本中并未定義或支持,可能因為我們使用的是較舊或較基礎的版本。在配置參數時需要結合實際 OpenGL 環境進行確認。


總結配置示例(紋理 2D):

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_ENV_MODE, GL_MODULATE);

我們關閉了所有 mipmapping 和過濾,設置為最近采樣模式;同時環繞模式設置為鉗制邊緣,避免紋理重復或鏡像。這些設置簡化了調試流程,確保渲染輸出清晰直觀,符合像素精度的顯示需求。

是否還需要我們整理一份完整的 glTexImage2D + glTexParameter 全流程配置參考?

在這里插入圖片描述

在這里插入圖片描述

運行游戲并確認我們已經做到了

我們已經快完成了整個紋理設置流程。當前的配置已經非常接近正確的狀態了,但仍需最后的核對和調整。

我們通過設置了一系列的 OpenGL 紋理參數,使得紋理渲染過程可以按照我們期望的方式工作。我們主要配置了以下幾個關鍵方面:


1. 紋理過濾模式(Texture Filter)

  • GL_TEXTURE_MIN_FILTER:當紋理被縮小時使用的過濾方式。我們設置為 GL_NEAREST,即使用最近鄰采樣,不進行雙線性插值或 Mipmap 等處理。這樣會讓圖像看起來更“銳利”,但可能產生鋸齒或閃爍感。
  • GL_TEXTURE_MAG_FILTER:當紋理被放大時使用的過濾方式,同樣設置為 GL_NEAREST,避免插值造成模糊。

我們選擇關閉所有 Mipmap(多級漸遠紋理)相關的設置,如 GL_TEXTURE_MIN_LODGL_TEXTURE_MAX_LODGL_TEXTURE_BASE_LEVELGL_TEXTURE_MAX_LEVEL 等。因為當前我們不希望啟用 Mipmap 功能,所以這些設置暫時不需要理會。


2. 紋理包裹模式(Texture Wrapping)

  • 設置了 GL_TEXTURE_WRAP_SGL_TEXTURE_WRAP_T(以及可能的 GL_TEXTURE_WRAP_R,用于三維紋理),用于定義當紋理坐標超出 [0,1] 范圍時的行為。
  • 我們選擇使用 GL_CLAMP_TO_EDGE,表示當紋理坐標超出范圍時,使用邊緣顏色進行擴展,不進行重復或鏡像。這樣可以避免不必要的重復圖案或拉伸變形。

3. 其他設置

  • 紋理優先級(GL_TEXTURE_PRIORITY):當前未設置,因為我們不需要考慮紋理被丟棄或緩存淘汰的情況。
  • OpenGL 版本兼容性問題:我們注意到部分紋理參數可能在當前使用的 OpenGL 版本中尚未支持或存在差異,需要根據實際情況確認支持情況或進行兼容性處理。

總結

整體配置已經非常接近目標,只差最后的驗證和細節修正。我們設定了基礎的紋理格式、大小、顏色通道、紋理過濾與包裹方式。接下來應進一步確認紋理數據在內存中的排列是否正確,比如 stride(步幅)、像素對齊方式是否與 glTexImage2D 的默認期望一致,必要時通過設置像素存儲參數(如 glPixelStorei)來顯式指定數據的對齊和步進行為,確保傳入的數據能夠被正確解釋并繪制。

整個過程雖然參數繁多,但每一項設置都為正確顯示紋理提供了保障。我們正在朝著最終目標逐步推進。

“這完全是謊言”

我們已經完成了整個 OpenGL 渲染設置流程,并且成功運行了游戲。這說明所有相關的參數都已經正確配置,OpenGL 可以順利讀取我們提供的紋理數據并將其渲染到屏幕上。

盡管我們沒有顯式指定紋理數據的打包方式(比如 stride 或像素對齊),但幸運的是,我們提供的緩沖區剛好以 OpenGL 默認所期望的方式進行了內存打包。也就是說,內存布局和 OpenGL 的讀取方式正好一致,從而避免了可能出現的顯示錯誤或圖像錯位問題。這種情況下,我們無需使用額外的像素存儲設置函數(例如 glPixelStorei)去調整讀取行為。

在確認一切正常后,我們還將畫面分辨率設置為 1920x1080,實現了全屏渲染,并成功進入游戲畫面。場景正常渲染,小角色也可以在場景中自由移動,說明整體渲染流程、紋理綁定、繪制調用等都已經穩定運行。

目前這只是第一輪完成基礎流程的嘗試。后續我們還需要逐步深入理解和完善各個環節,包括:

  • 更細致地控制紋理數據的上傳和管理;
  • 更全面地理解 OpenGL 的狀態設置和渲染流程;
  • 引入更多的游戲渲染邏輯,而不僅僅是將一張圖像貼到屏幕上;
  • 研究和實現更復雜的渲染技術,如著色器、燈光、陰影、動態效果等。

這次的實現展示了從初始化 OpenGL 到完整渲染流程的一條基本路徑,也為后續進一步構建完整游戲渲染系統打下了基礎。

至此,我們順利完成了基本的 OpenGL 環境搭建、紋理上傳與參數配置、最終顯示到屏幕等關鍵步驟,整個渲染流程驗證成功。我們將在此基礎上逐步擴展和提升渲染能力。

在這里插入圖片描述

win32_game.cpp: 引入全局變量 GLuint BlitTextureHandle

我們暫時先去掉了一部分代碼,當前階段其實并不需要保留它。雖然遲早都要處理這部分邏輯,但目前可以先跳過,專注于更關鍵的部分。

我們知道,在初始化 OpenGL 的時候,一旦創建了有效的渲染上下文(Context),就可以立即生成紋理對象(使用 glGenTextures)。這實際上是我們需要做的最基本的事情,一旦有了上下文,紋理的生成就可以立刻完成。

之所以之前沒有這么做,是因為當時不想額外處理這部分內容,但這不是一個永久的狀態,之后肯定要把這塊邏輯加回來。

關于紋理的初始化,我們應該盡早執行,在有了 OpenGL 上下文之后盡快調用紋理生成和配置的代碼,可以確保渲染流程更加清晰且穩定。當前的做法只是為了快速推進整體流程,因此對部分步驟進行了臨時跳過,但這不是最終方案,后續必須清理和調整這些臨時代碼,確保結構清晰、邏輯正確。
在這里插入圖片描述

我們能否以某種方式驗證垂直同步(vsync)?

我們目前尚未啟用垂直同步(VSync),因此也就沒有任何可以驗證的內容。換句話說,現在并沒有讓 OpenGL 對渲染進行垂直同步的請求,所以此時還無法判斷或測試其是否生效。

垂直同步的相關設置會在后續步驟中加入。只有在我們顯式請求 OpenGL 啟用 VSync 的時候,它才會開始起作用,也就是說只有在明確告訴它要進行幀率同步之后,才需要去驗證是否有效。

當前階段的渲染過程沒有啟用 VSync,因此圖像刷新可能不會與顯示器的刷新率保持一致。實際渲染中是否發生撕裂或卡頓等現象,取決于系統和驅動默認的設置,但由于我們并未明確要求 VSync,因此這些行為不是我們控制范圍內的事情,后續將專門設置和處理。

從 u,v 坐標到屏幕坐標轉換時是否會有拉伸問題?如果有,你會如何修復?

在將紋理的 UV 坐標映射到屏幕坐標的過程中,確實可能會出現拉伸(Stretching)問題。雖然通常我們會將單位立方體(unit cube)直接映射到視口(viewport)上,從而實現一一對應的關系,但必須明確一點:前提是所有數學變換正確,紋理坐標和頂點坐標匹配,才能避免視覺上的變形。

你綁定的紋理名稱在第一次迭代后總是為零

在迭代過程中,有人提到綁定(binding)始終在第一次迭代后為 0,但這個說法并不準確。至少在我們所使用的 OpenGL 版本中,情況并非如此。

在舊版本的 OpenGL 中,確實存在某些行為讓人誤解為綁定值在迭代后會自動變為 0,但這通常是由于上下文狀態管理不當導致的。例如,如果我們沒有明確設置或更新綁定目標,OpenGL 會維持先前的綁定狀態,這可能會給人一種“總是為 0”的錯覺。

實際上,只要我們在每次使用紋理(或其他可綁定資源)前,明確調用如 glBindTexture 或類似綁定函數,并傳入正確的紋理 ID,就不會出現綁定始終為 0 的問題。需要特別注意的是:

  • OpenGL 是一個狀態機,如果我們在某一幀之后沒有重新設置綁定,它會繼續使用上一次的狀態;
  • 若綁定目標未初始化或被錯誤釋放,可能導致其值不可用或者表現出為 0;
  • 在某些調試或驅動工具下,如果資源生命周期沒管理好,也可能觀察到綁定失敗或被清零。

因此,我們必須確保在每一幀或每一次繪制調用之前,正確并明確地綁定需要的資源,避免依賴任何隱式狀態。只要狀態設置得當,綁定的值在迭代中不會無故變為 0。關鍵在于清晰地管理上下文狀態和資源生命周期。

我想在舊版 OpenGL 中,你甚至不需要 glGenTextures,你可以選擇任意的整數

關于是否可以繞過 glGenTextures 而直接使用任意整數作為紋理 ID,這是不正確的。我們必須調用 glGenTextures 來生成合法的紋理對象 ID。這個函數的作用是讓 OpenGL 為我們分配并初始化一個有效的紋理對象,并返回一個可以使用的紋理句柄(通常是一個整數)。這個 ID 是 OpenGL 內部管理資源的關鍵,它并不是一個可以隨意指定的任意數值。

嘗試直接使用一個自己硬編碼的整數(比如 42123)作為紋理 ID 來進行綁定,例如:

glBindTexture(GL_TEXTURE_2D, 42);

雖然在某些驅動或調試環境下不會立即報錯,但這并不會創建一個實際有效的紋理對象。OpenGL 規范要求,只有通過 glGenTextures 獲取的 ID,才會被視為合法的紋理對象。否則:

  • 綁定的紋理對象是未定義的行為;
  • 很可能不會觸發任何圖像上傳或繪制效果;
  • 在啟用調試輸出的環境下可能會看到相關警告或錯誤。

因此,必須使用:

GLuint texID;
glGenTextures(1, &texID);

來獲得一個有效的紋理對象句柄,然后才能對其調用 glBindTextureglTexImage2D 等函數進行配置和使用。

總結:

  • 不能隨意指定整數作為紋理 ID
  • 必須使用 glGenTextures 來獲得合法 ID
  • 這是 OpenGL 內部資源管理的標準機制

正確使用這一機制是確保渲染正確性和資源穩定性的基礎。

win32_game.cpp: 設置 GlobalBlitTextureHandle = 1; 而不是使用 glGenTextures

在討論是否需要調用 glGenTextures 生成紋理對象時,出現了對這個過程的疑問。一些實驗表明,如果直接將紋理 ID 設置為一個固定的數字,比如 1,可能會出現問題。這樣做可能導致綁定的紋理對象是默認的紋理(ID 為 0),這意味著實際上并沒有創建有效的紋理對象。

為了驗證這個問題,提出了一個測試方案:嘗試在提交一個紋理之后,再設置另一個紋理 ID(例如 10),并查看是否能得到一個空白的紋理效果。如果可以成功顯示一個白色紋理,這意味著 ID 為 10 的紋理并沒有內容,這樣的測試可以幫助驗證是否需要 glGenTextures

結果顯示,若沒有使用 glGenTextures 生成有效的紋理對象,那么直接設置一個數字可能無法確保該紋理有效。這也讓人質疑是否 glGenTextures 真的沒必要調用。實際上,glGenTextures 的作用是確保分配出有效的紋理對象,并返回一個合法的紋理 ID。因此,即使有人提出直接使用固定的紋理 ID,還是需要依賴 glGenTextures 來確保紋理的合法性。

總結來說:

  1. 不能隨意使用任意數字作為紋理 ID,必須通過 glGenTextures 來生成一個合法的紋理對象。
  2. glGenTextures 的作用是生成有效的紋理 ID,確保 OpenGL 能夠管理這些紋理資源。
  3. 盡管直接使用紋理 ID(如 1 或 10)似乎能正常工作,但這種做法并不符合 OpenGL 的規范,也有可能導致不可預期的行為。
  4. 理論上,如果不調用 glGenTextures,設置的紋理 ID 可能會指向一個未初始化的紋理,進而導致渲染出錯或表現不正確。

因此,正確的做法依然是調用 glGenTextures 來生成紋理對象。

當圖像和紋理大小相同的時候,紋理過濾(->GL_NEAREST)會發生嗎?

在使用 GL_NEAREST 進行紋理過濾時,即使紋理圖像和顯示的大小完全一致,過濾操作仍然會發生。具體來說,如果選擇了 GL_LINEAR 作為紋理過濾方式,表示應用雙線性過濾,那么在紋理的每個像素采樣時,系統會計算周圍四個像素的加權平均值。然而,如果紋理和顯示大小完全一致,雙線性過濾的計算結果會變成一種特殊情況,其中權重系數變成 01,即 0, 0, 0, 1

這種情況下,雖然雙線性過濾的計算依舊被執行,但因為權重系數的設置,最終效果與 GL_NEAREST(最近點采樣)一樣,看起來仿佛沒有任何變化。在理論上,如果硬件沒有問題,這樣的計算會確保正確的采樣,但實際上仍然會應用過濾操作,只是結果與沒有應用過濾時相同。這個過程的關鍵在于過濾本身始終會執行,但實際視覺效果可能看起來沒什么不同,除非紋理和屏幕大小不一致。

在你的職業生涯中,你偏好使用哪個 GPU 庫,例如 OpenGL、DirectX、GLSL 等?你現在使用的是哪個,為什么?

在職業生涯中,雖然我傾向于使用 OpenGL,因為它具有跨平臺的優勢,但實際上,我并不喜歡任何 GPU 庫。無論是 OpenGL、DirectX 還是 GLSL,它們都有各自的問題,都不是我設計的方式。它們的設計方式并不完全符合我的期望,存在很多不足之處。因此,盡管 OpenGL 在跨平臺性上有一定的優勢,但我對這些工具的設計并不感到滿意。

等我們升級到更現代的 OpenGL 后,游戲會先渲染到幀緩沖區,然后再渲染到相同的三角形,還是直接渲染到主窗口緩沖區?

在升級到更現代的 OpenGL 時,渲染過程是否通過幀緩沖進行,取決于是否需要后處理效果。如果有后處理效果,那么首先需要將游戲渲染到一個緩沖區中,之后對該緩沖區進行一些效果處理,通常是通過緩沖區鏈來實現,而不是常見的“乒乓”緩沖。最終的步驟是將緩沖區的內容解析出來,顯示到屏幕上。

換句話說,如果沒有后處理效果,通常會直接將游戲渲染到主窗口緩沖區。但如果涉及到圖像后處理,首先渲染到一個中間緩沖區,應用效果后,再將最終結果顯示到屏幕上。

在你移除 Init 變量并內聯初始化紋理之前,你總是將 0 作為名稱傳遞,順便提一下

在代碼中,提到的“單位變量”和“內聯”可能是關于紋理名稱的處理,指出始終傳遞零作為紋理名稱。這個問題的原因可能是忘記在紋理變量前面加上 static 關鍵字,導致變量沒有被正確處理為靜態變量。通過加上 static,變量的生命周期和作用域才會正確管理,從而避免問題的發生。

那么現在就沒有辦法直接將圖像復制到后臺緩沖區了嗎?

目前,直接向顯卡的后備緩沖區(back buffer)進行繪制的方法已經不再常見。在過去的 DirectDraw 日子里,通過將顯存映射到主內存,CPU 可以直接寫入顯卡的內存,這樣雖然數據還是通過 PCI 總線傳輸,但看起來好像是 CPU 直接寫入顯卡內存。但實際上,數據依然需要通過 PCI 總線傳輸。

如今,隨著圖形處理技術的進步,圖形處理變得更加異步,直接將數據寫入顯卡內存已經不再是一個高效的做法。現代的做法是將數據先存儲在內存中的某個地方,然后告訴顯卡去獲取這些數據,這種方式比直接寫入顯卡內存更高效。

當然,理論上還是可以通過編寫特殊的驅動程序來實現直接寫入顯卡內存,將 GPU 內存映射到主內存,允許 CPU 直接向其寫入,但這種方式效率較低,已經不再是主流方法。

現在我們使用 OpenGL 將緩沖區移到 GPU 上,速度上有區別嗎?

使用 OpenGL 將緩沖區移到 GPU 上,與之前的做法相比,速度差異并不顯著。雖然沒有做精確的性能測量,但從實際表現來看,速度差距較小,甚至可能會稍微變慢一些。這是因為現在的渲染方式可能更難預測,導致性能上有所波動。

不過,如果不進行初始化而直接進行圖像的轉換(blit)操作,渲染的速度可能會有所改變。另一個需要注意的因素是雙緩沖(double buffering)被關閉了,這可能會影響渲染的平滑度。如果啟用雙緩沖,可能會提高渲染的效率和流暢度。

此外,由于流式捕捉的原因,也有可能導致性能受到影響。整體而言,這種方法可能不是最理想的渲染方式,特別是在需要高效渲染時。

你知道如何使用 OpenGL 優化 CPU 和 GPU 之間的 PCI 傳輸嗎?

在使用 OpenGL 進行 PCI 傳輸優化時,實際上并沒有太多可以顯著優化的空間。原因在于,基本上只是將紋理傳送到 GPU 并一次性繪制,整個過程由驅動程序處理得相當高效。

如果一定要優化,唯一可能的方法是減少不必要的內存復制操作。假設關注的是內存傳輸的時間,可以嘗試通過多線程來重疊執行。例如,使用一個線程來處理圖像數據的傳輸,同時在另一個線程上進行渲染工作(如游戲模擬)。這樣可以在紋理傳輸的同時進行其他工作,從而減少等待時間,雖然并不會加速傳輸本身。

此外,早期有報道指出,通過 GPU 鎖定內存并寫入數據的方式其實比直接傳輸慢,因為這種方法會在驅動中產生同步點,導致 GPU 和驅動之間需要協調內存訪問。而直接傳輸(如通過 OpenGL)通常更高效。

不過,這種內存傳輸操作并不是系統中的性能瓶頸,因此優化這部分的收益并不會非常大。所以雖然可以通過重疊操作來優化整體流程,但對性能的影響相對有限。

我不知道這個問題是否適用于上一個問題,我剛剛才到,但 glTexSubImage2D 可能比 glTexImage2D 更快

在討論是否使用 glTexSubImage2D 替代 glTexImage2D 時,通常認為 glTexSubImage2D 會比 glTexImage2D 快,然而這實際上可能并非如此。因為在使用 glTexSubImage2D 時,顯卡能夠檢測到紋理并不被替換,這意味著可能會變得更慢。

簡單來說,glTexSubImage2D 在某些情況下可能導致性能下降,因為顯卡通常會優化紋理替換操作,而如果它認為紋理沒有發生變化,可能會導致它進行額外的檢查或操作。因此,從性能角度來看,直接替換紋理的操作可能更為高效。

總體來說,雖然看起來替換紋理可能更快,但實際上在某些情況下,顯卡的優化機制可能使得 glTexSubImage2D glTexImage2D 更慢。

Blackboard: glTexImage2D vs glTexSubImage2D

在討論 glTexImage2DglTexSubImage2D 時,重點在于兩者的效率差異以及依賴關系的影響。

glTexImage2DglTexSubImage2D 的區別

  • glTexImage2D:它會完全替換紋理內容,每次調用都會重新分配內存并復制整個圖像數據到紋理中。所有的紋理數據都會被覆蓋,并且會創建一個新的依賴鏈,所有依賴于該紋理的操作都會等待紋理更新完成后才會繼續執行。
  • glTexSubImage2D:與 glTexImage2D 不同,glTexSubImage2D 只替換紋理中的一部分數據,其余的紋理內容保持不變。這意味著更新部分數據時,其他部分的紋理可以繼續使用,依賴鏈更為復雜,更新操作需要等到紋理更新完成才能繼續進行。

為什么 glTexSubImage2D 可能不如 glTexImage2D 高效

雖然 glTexSubImage2D 可以更新紋理的部分內容,理論上看起來比 glTexImage2D 更高效,但是由于它在創建依賴鏈時的復雜性,反而可能導致性能下降。具體原因包括:

  1. 依賴鏈增加:如果使用 glTexSubImage2D 更新紋理的部分內容,驅動程序會生成更多的依賴鏈,紋理的更新會受到影響,其他操作會等待紋理更新完成。這種依賴關系的增加會使得 GPU 的任務處理變得更加串行化,減少并行度,從而影響整體效率。
  2. 更長的等待時間:因為在更新紋理時,GPU 需要等待 glTexSubImage2D 完成,所有依賴這個紋理的操作都會被延遲執行,造成更長的等待時間,影響渲染效率。

對比 glTexImage2DglTexSubImage2D

  • glTexImage2D 重新創建整個紋理,允許 GPU 更好地并行處理后續任務,因此通常更高效,尤其是在處理完整的紋理替換時。
  • glTexSubImage2D 盡管只更新紋理的一部分,但由于產生的依賴關系可能導致更多的串行化執行,因此通常不如 glTexImage2D 高效,尤其在需要頻繁更新紋理時。

在實際開發中的應用

  • 如果是完全替換紋理數據,glTexImage2D 更合適,因為它的執行不需要復雜的依賴關系管理。
  • 如果只更新部分紋理,理論上 glTexSubImage2D 應該更高效,但在某些情況下可能因為依賴鏈過長而導致性能下降。
  • 需要注意的是,實際效果還要依賴于具體的硬件和驅動程序的實現。為了得到更好的性能,通常需要根據具體的情況進行測量和優化,可能需要使用不同的紋理更新策略(例如使用雙緩沖或并行執行)。

總結

glTexSubImage2D 可以在更新紋理的一部分時避免重新分配整個紋理的內存,但由于增加了依賴鏈,可能會導致更低的并行性,從而影響性能。相對來說,glTexImage2D 在完全替換紋理時可能會更高效。

在那一連串的 gl 命令中,顯卡究竟在什么時刻參與進來?是命令依賴的嗎?還是驅動程序依賴的?

在 OpenGL 命令的執行過程中,GPU 介入的時機和方式主要取決于 驅動程序。具體來說,GPU 的參與時機完全依賴于驅動程序如何優化和管理渲染過程。驅動程序負責決定如何調度和處理圖形命令的執行順序,可能會進行某些優化,如批處理、延遲執行或并行處理等。

此外,雖然硬件本身(GPU)也會對優化和執行產生影響,但大部分的優化決策和執行安排都是由驅動程序控制的。因此,GPU 的參與實際上是由驅動程序的實現和選擇的優化策略決定的,而不是固定的硬件行為。不同的驅動程序可能會有不同的策略和處理流程,因此,GPU 參與的時機也可能有所不同。

總結來說,GPU 何時開始介入渲染任務,完全由驅動程序的設計和優化決定,而驅動程序又根據不同的硬件特性進行相應的調整。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/79955.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/79955.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/79955.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

(二)mac中Grafana監控Linux上的MySQL(Mysqld_exporter)

框架:GrafanaPrometheusMysqld_exporter 一、監控查看端安裝 Grafana安裝-CSDN博客 普羅米修斯Prometheus監控安裝(mac)-CSDN博客 1.啟動Grafana服務 brew services start grafana 打開瀏覽器輸入http://localhost:3000進入grafana登錄…

GitHub 趨勢日報 (2025年04月17日)

本日報由 TrendForge 系統生成 https://trendforge.devlive.org/ 📈 今日整體趨勢 Top 10 排名項目名稱項目描述今日獲星總星數語言1Anduin2017/HowToCook程序員在家做飯方法指南。Programmer’s guide about how to cook at home (Simplified Chinese onl…? 224…

(一)mac中Grafana監控Linux上的CPU等(Node_exporter 安裝使用)

框架:GrafanaPrometheusNode_exporter 機器狀態監控(監控服務器CPU,硬盤,網絡等狀態) Node_exporter安裝在被測服務器上,啟動服務 各步驟的IP地址要換為被測服務器的IP地址Prometheus.yml的 targets值網頁訪問的ip部分grafana添加數據源的…

java IO/NIO/AIO

(?▽?)曼波~~~~!讓曼波用最可愛的賽馬娘方式給你講解吧!(? ???ω??? ?) 🎠曼波思維導圖大沖刺(先看框架再看細節哦): 📚 解釋 Java 中 IO、NIO、AIO 的區別和適用場景: …

Silverlight發展歷程(微軟2021年已經停止支持Silverlight 5)

文章目錄 Microsoft Silverlight 發展歷程引言起源與背景(2006-2007)互聯網技術格局與微軟的挑戰WPF/E 項目的啟動 Silverlight 1.0 的誕生(2007)正式命名與首次發布初步的市場定位 Silverlight 2.0:真正的突破&#x…

【大數據、數據開發與數據分析面試題匯總(含答案)】

在大數據、數據開發與數據分析領域的面試中,扎實掌握各類知識點至關重要。以下是精心整理的面試題,涵蓋單選題和多選題,助你備考一臂之力。 試題目錄 大數據、數據開發與數據分析高頻面試題解析1. 數據倉庫分層架構設計2. 維度建模與范式建模…

Docker部署禪道21.6開源版本

將數據庫相關環境變量分開,增加注釋或空格使得命令更易讀。 如果你的 MySQL 主機、端口等配置沒有變化,應該確保這些信息是安全的,并考慮使用 Docker secrets 或環境變量配置來避免直接暴露敏感信息。 docker run -d -it --privilegedtrue …

Yocto項目實戰教程 · 第4章:4.2小節-菜譜

🔍 B站相應的視頻教程: 📌 Yocto項目實戰教程-第4章-4.2小節-菜譜 記得三連,標為原始粉絲。 在 Yocto 項目中,**菜譜(Recipe)**承載了包的配置信息、源碼獲取方式、編譯與安裝步驟,是…

【pytorch】torch.nn.Unfold操作

說明 一個代碼里涉及到了unfold的操作,看了半天官網都沒整明白維度怎么變化的,參考這個鏈接搞明白了: https://blog.csdn.net/ViatorSun/article/details/119940759 https://zhuanlan.zhihu.com/p/361140988 維度計算 輸入( N,…

Linux 固定IP地址

一.查看網口狀態: $ ip a 二.配置靜態IP文件: $ sudo vi /etc/network/interface auto eth0 iface eth0 inet static address 192.168.0.252 gateway 192.168.0.1 netmask 255.255.255.0 #network 192.168.0.0 #broadcast 192.168.0.255 三.重啟網卡讓新…

android的 framework 有哪些知識點和應用場景

Android Framework 知識點 1. 四大組件 Activity(活動) 是 Android 應用中最基本的組件,用于實現用戶界面。一個 Activity 通常對應一個屏幕的內容。有自己的生命周期,包括 onCreate、onStart、onResume、onPause、onStop、onDe…

如何在PDF.js中改造viewer.html以實現PDF的動態加載

在PDF.js中改造viewer.html實現PDF動態加載,需結合參數傳遞、文件流處理及跨域配置等技術。以下是綜合多個技術方案的核心實現步驟: ?一、基礎參數傳遞法? 1. ?URL參數動態加載? 通過修改viewer.html的URL參數傳遞PDF路徑,適用于靜態文…

組件之間的數據通信方式

Vue 的傳值方式(即組件之間的數據通信方式)根據組件關系不同(父子、兄弟、跨層級)有所區別。下面是常見的傳值方式,按使用場景來分類: 一、父子組件傳值 1. props(父 -> 子) 父…

組件是怎樣寫的(1):虛擬列表-VirtualList

本篇文章是《組件是怎樣寫的》系列文章的第一篇,該系列文章主要說一下各組件實現的具體邏輯,組件種類取自 element-plus 和 antd 組件庫。 每個組件都會有 vue 和 react 兩種實現方式,可以點擊 https://hhk-png.github.io/components-show/ …

個性化的配置AndroidStudio

Android Studio 提供諸多向導和模板,可用于驗證 Java 開發套件 (JDK) 和可用 RAM 等系統要求,以及配置默認設置,例如經過優化的默認 Android 虛擬設備 (AVD) 模擬和更新的系統映像。本文檔介紹了可用于自定義 Android Studio 使用方式的其他配…

人類行為的原動力是自我保存-來自ChatGPT

自我保存(Self-Preservation)確實可以說是人類行為最原始、最底層的驅動力。 簡單來說: 無論我們做什么,表面看動機五花八門,實際上歸根到底都繞不開活下去、保護自己。 💡 從不同層面理解這個觀點&#…

SystemVerilog語法之內建數據類型

簡介:SystemVerilog引進了一些新的數據類型,具有以下的優點:(1)雙狀態數據類型,更好的性能,更低的內存消耗;(2)隊列、動態和關聯數組,減少內存消耗…

藍光三維掃描技術:高效精密測量相機鏡頭底座注塑件

如今越來越多的攝影愛好者、vlog拍攝者使用數碼相機以及無人機,隨時隨地記錄生活中的每一刻美好瞬間,對相機設備的要求也不斷提高。 — 案例背景 — 相機鏡頭底座涉及鏡頭裝置可靠、螺絲位置度連接以及殼體組裝,鏡頭底座注塑件生產廠商&…

【前端】【面試】【業務場景】前端如何獲取并生成設備唯一標識

? 總結 問題:前端如何獲取并生成設備唯一標識? 核心要點:瀏覽器原生信息有限,但通過組合多個維度可生成設備指紋(Device Fingerprint),用于唯一標識設備。 常見方式: 瀏覽器信息&…

極刻AI搜v1.0 問一次問題 AI工具一起答

軟件名:極刻AI搜 版本:v1.0 功能:囊括了互聯網上比較好用的一些支持”搜索“的網站或者工具 開發平臺:nodepythonweb 分類有: AI搜索(支持智能問答的AI搜索引擎) 常規搜索:&#xff…