圖形管道
在OpenGL
中所有物體處在3D
空間中,但屏幕和窗口是一個2D
像素數組,因此OpenGL
工作的很大一部分是將所有3D
坐標轉換為適合您屏幕上的2D
像素。將3D
坐標轉換為2D
像素的過程由OpenGL
的圖形管道管理。圖形管道可分為兩大部分:第一部分將3D
坐標轉換為2D
坐標,第二部分將2D
坐標轉換為實際彩色像素。在本教程中,我們將簡要討論圖形管道,以及如何利用它來創建花哨的像素。
圖形管道將一組3D
坐標作為輸入,並將這些坐標轉換為屏幕上的彩色2D
像素。圖形管道可分為幾個步驟,其中每個步驟都需要上一步的輸出作為輸入。所有這些步驟都是高度專業化的(它們具有一個特定的功能),並且可以很容易地並行執行。由於其具有並行性特點,當今的圖形卡具有數千個小型處理內核,通過為管道的每個步驟在GPU
上運行小型程序,在圖形管道中快速處理數據。這些小程序稱為着色器。
其中一些着色器由開發人員配置,這允許我們編寫自己的着色器來替換現有的默認着色器。這為我們提供了對管道特定部分的更細粒度的控制,並且由於它們在GPU
上運行,因此還可以為我們節省寶貴的CPU
時間。着色器以OpenGL
着色語言(GLSL
)編寫,我們將在下一教程中深入探討這一點。
下面您將找到圖形管道所有階段的抽象表示形式。
具有藍色背景的部分是可編程的,並且具有灰色背景的部分可以使用函數輕輕自定義。步驟如下:
- 頂點着色器:頂點移動到位置。這是應用模型位置等位置的位置。
- 形狀拼接。在這個階段,
OpenGL
的工作原理是將頂點拼接到三角形中; - 幾何着色器:過程的可選階段。允許您從形狀裝配體微調結果。
- 柵格化:三角形轉換為碎片。
- 線段着色器:對線段進行修改,以包括顏色數據等內容。這是紋理和照明,除其他外,應用的地方。
- 測試和混合:片段着色器的結果與場景的其余部分集成。
這些可能看起來很繁瑣,但一旦設置完成,我們進入管道,它是相當直觀的。
一些新的函數
我們需要重寫幾個額外的函數才能開始。首先,我們重寫OnLoad
函數。
protected override void OnLoad(EventArgs e) { GL.ClearColor(0.2f, 0.3f, 0.3f, 1.0f); base.OnLoad(e); }
當窗口首次打開時,此函數將運行一次。任何初始化相關的代碼都應轉到此處。
同時在這里,我們得到我們OpenGL
調用的第一個函數:GL.ClearColor
。這需要四個浮點,范圍在0.0f
和1.0f
之間。這將決定在窗口在幀之間清除后的顏色。
之后,我們需要重寫OnRenderFrame
。
protected override void OnRenderFrame(FrameEventArgs e) { GL.Clear(ClearBufferMask.ColorBufferBit); Context.SwapBuffers(); base.OnRenderFrame(e); }
我們這里有兩個調用。首先,GL.Clear
使用OnLoad
中設置的顏色清除屏幕。這應始終是呈現時調用的第一個函數。
之后我們使用Context.SwapBuffers
。幾乎任何現代OpenGL
上下文都是所謂的"雙緩沖"。雙緩沖意味着OpenGL
繪制到的兩個領域。本質上:顯示一個區域,而另一個區域用來展示。然后,當您調用交換緩沖區時,兩者將反轉。單緩沖上下文可能會有屏幕卡頓等問題。
現在我們重寫OnResize
protected override void OnResize(EventArgs e) { GL.Viewport(0, 0, Width, Height); base.OnResize(e); }
每次調整窗口大小時,都會運行此功能。GL.Viewport
將NDC
映射到窗口。OnResize
不是非常重要,除了我們已經寫入的,這里后期將不會添加任何代碼。
頂點輸入
要開始繪制某些數據,我們必須首先給OpenGL
一些輸入頂點數據。OpenGL
是一個3D
圖形庫,因此我們在OpenGL
中指定的所有坐標都位於3D
(x
、y
和z
坐標中)。OpenGL
不會簡單地將所有3D
坐標轉換為屏幕上的2D
像素;當 3 個軸(x
、y
和z
)上的3D
坐標在 -1.0 和 1.0 之間的特定范圍內時,OpenGL
才處理它們。此所謂的規范化設備坐標范圍內的所有坐標最終都將在屏幕上可見(並且該區域外的所有坐標不會)。
因為我們想要渲染一個三角形,所以我們要指定三個頂點,每個頂點都有一個3D
位置。我們在浮動數組中的規范化設備坐標(OpenGL
的可見區域)中定義它們。在類中作為屬性來表示:
private float[] vertices = { -0.5f, -0.5f, 0.0f, // Bottom-left vertex 0.5f, -0.5f, 0.0f, // Bottom-right vertex 0.0f, 0.5f, 0.0f // Top vertex };
由於OpenGL
在3D
空間中工作,因此我們渲染一個2D
三角形,每個頂點具有0.0
的z
坐標。這樣,三角形的深度保持不變,使其看起來像是2D
。\
規范化設備坐標(NDC)
在頂點着色器中處理頂點坐標后,它們應位於規范化設備坐標中,這是 x、y 和 z 值從 -1.0 到 1.0 變化的一個小空間。超出此范圍的任何坐標都將被丟棄/剪切,並且在屏幕上不可見。下面你可以看到我們在規范化設備坐標中指定的三角形(忽略 z 軸):
與通常的屏幕不同,屏幕坐標是向上方向的正y
軸點,而(0,0)
坐標位於圖形的中心,而不是左上角。最終,您希望所有(已轉換的)坐標最終到達此坐標空間中,否則它們將不可見。
然后,使用GL.Viewport
提供的數據,通過視口變換將NDC
坐標轉換為屏幕空間坐標。然后,生成的屏幕空間坐標將轉換為片段,作為片段着色器的輸入。
緩沖區
定義頂點數據后,我們希望將其作為輸入發送到圖形管道的第一個過程:頂點着色器。這是通過創建GPU
上的內存來完成的,我們在其中存儲頂點數據,配置OpenGL
應如何解釋內存,並指定如何將數據發送到圖形卡。然后,頂點着色器或通過我們告訴它的信息,從而在它的內存中處理盡可能多的頂點。
我們通過所謂的頂點緩沖對象(VBO
)管理此內存,該對象可以在GPU
的內存中存儲大量頂點。使用這些緩沖對象的優點是,我們可以一次向圖形卡發送大量數據,而無需一次發送頂點數據。從CPU
將數據發送到顯卡相對緩慢,因此,只要我們可能,我們嘗試一次發送盡可能多的數據。一旦數據進入顯卡的內存,頂點着色器幾乎可以即時訪問頂點,使其非常快。
頂點緩沖區對象是我們第一次出現OpenGL
對象,正如我們在OpenGL
教程中討論過的。就像OpenGL
中的任何對象一樣,此緩沖區具有與該緩沖區對應的唯一ID
,因此我們可以使用GL.GenBuffers
函數生成具有緩沖區ID
的ID
。
向Game
類添加int
用來存儲句柄:
int VertexBufferObject;
之后在OnLoad
函數內添加這一行:
VertexBufferObject = GL.GenBuffer();
OpenGL
具有多種類型的緩沖區對象,具有頂點緩沖區對象的緩沖區類型為BufferTarget.ArrayBuffer
。OpenGL
允許我們同時綁定到多個緩沖區,只要它們具有不同的緩沖區類型。我們可以使用GL.BindBuffer
函數將新創建的緩沖區綁定到BufferTarget.ArrayBuffer
:
GL.BindBuffer(BufferTarget.ArrayBuffer, VertexBufferObject);
從該點開始,我們進行的任何緩沖區調用(在BufferTarget.ArrayBuffer
上)將用於配置當前綁定的緩沖區,即VertexBufferObject
。然后我們可以調用GL.BufferData
。將以前定義的頂點數據復制到緩沖區內存中的緩沖區數據函數:
GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * sizeof(float), vertices, BufferUsageHint.StaticDraw);
GL.BufferData
是一個專門用於將用戶定義的數據復制到當前綁定的緩沖區的函數。它的第一個參數是我們要將數據復制到的緩沖區的類型:當前綁定到BufferTarget.ArrayBuffer
區目標的頂點緩沖區對象。第二個參數指定要傳遞給緩沖區的數據的大小(以字節為單位);數據類型的簡單大小,乘以頂點的長度,就足夠了。第三個參數是我們想要發送的實際數據。
第四個參數是BufferUsageHint
,它指定我們希望圖形卡如何管理給定的數據。這有 3 種形式:
StaticDraw: 數據很可能不改變或者改變的很少.
DynamicDraw: 數據可能會改變很多.
StreamDraw: 每次繪制數據時都會更改
三角形的位置數據不會更改,並且對於每個渲染調用都保持不變,因此其使用類型最好為StaticDraw
。如果有一個緩沖區,其數據可能會頻繁更改,則DynamicDraw
或StreamDraw
的使用類型可確保圖形卡將數據放在內存中,從而允許更快的寫入速度。
注意:當編程結束時,我們需要手動清除緩沖區。為此,我們需要添加以下函數:
protected override void OnUnload(EventArgs e) { GL.BindBuffer(BufferTarget.ArrayBuffer, 0); GL.DeleteBuffer(VertexBufferObject); base.OnUnload(e); }
將緩沖區綁定到 0 基本上會將其設置為 null,因此,任何修改緩沖區而不首先綁定緩沖區的調用都會導致崩潰。這比意外修改不希望修改的緩沖區更容易調試。
到目前為止,我們存儲了圖形卡內存中的頂點數據,由名為 VBO
的頂點緩沖區對象管理。接下來,我們要創建一個頂點和片段着色器,實際處理這些數據,所以讓我們開始構建這些
着色
現在,我們已經擁有了數據,是時候創建我們的管道了。為此,我們創建頂點着色器和線段着色器。
頂點着色器是像我們這樣的人可編程的着色器之一。現代OpenGL
要求我們至少設置一個頂點和片段着色器,如果我們想要做一些渲染,我們將簡要介紹着色器,並配置兩個非常簡單的着色器來繪制我們的第一個三角形。在下一教程中,我們將更詳細地討論着色器。
我們需要做的第一件事是在着色器語言GLSL
(OpenGL
着色語言)中編寫頂點着色器,然后編譯此着色器,以便我們可以在應用程序中使用它。下面您將在GLSL
中找到非常基本的頂點着色器的源代碼:
#version 330 core layout (location = 0) in vec3 aPosition void main() { gl_Position = vec4(aPosition, 1.0); }
將之另存為shader.vert
如您所看到的,GLSL
看起來與C
類似。每個着色器以其版本的聲明開頭。由於OpenGL 3.3
及更高版本號與OpenGL
的版本號匹配(例如,GLSL
版本420
對應於OpenGL
版本4.2
)。我們還明確提到我們使用的核心配置文件功能。
接下來,我們使用in
關鍵字聲明頂點着色器中的所有輸入頂點屬性。現在,我們只關心位置數據,因此我們只需要一個頂點屬性。GLSL
具有一個矢量數據類型,該數據類型基於其后綴數字包含1到4個浮點。由於每個頂點都有一個3D
坐標,因此我們創建一個帶aPosition
的vec3
輸入變量。我們還通過布局(location = 0
)專門設置輸入變量的位置,稍后您將看到為什么我們需要該位置。
每個着色器的入口點都是void main()
函數。在這里您可以根據自己所需做任何處理。但是,在這里,我們只需將一個用於頂點着色器的內置的、表示該頂點的最終位置的變量gl_Position
進行賦值。但是,gl_Position
是一個 vec4
,但我們的輸入頂點是一個 vec3
。為此,我們使用函數vec4
使向量足夠長。
當前頂點着色器可能是我們可以想象到的最簡單的頂點着色器,因為我們沒有處理任何輸入數據,只是將它轉發到着色器的輸出。在實際應用中,輸入數據通常尚未在規范化的設備坐標中,因此我們首先必須轉換輸入數據以使位於OpenGL
可見區域內的坐標。
片段着色器是我們要為渲染三角形而創建的第二個也是最后一個着色器。片段着色器用於計算像素的顏色輸出。為了簡單,片段着色器將始終輸出橙色。
計算機圖形中的顏色表示為4個值的矢量:紅色、綠色、藍色和alpha
(不透明度)分量,通常縮寫為 RGBA
。在 OpenGL
或GLSL
中定義顏色時,我們將每個組件的強度設置為介於0.0和 1.0 之間的值。例如,如果我們將紅色設置為 1.0f,將綠色設置為 1.0f,我們就會得到兩種顏色的混合物,並得到黃色。通過這3種顏色組件,我們可以產生超過1600萬種不同的顏色!
#version 330 core out vec4 FragColor; void main() { FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); }
將之另存為shader.frag
片段着色器只需要一個輸出變量,它是大小 4 的矢量,用於定義我們應該自己計算的最終顏色輸出。我們可以用 out 關鍵字聲明輸出值,我們在這里立即命名為 FragColor
。接下來,我們只需將vec4
分配給顏色輸出,作為橙色,Alpha
值為 1.0(1.0 完全不透明)。
編譯着色器
我們有着色器源,但現在我們需要編譯着色器。這在運行時完成;無法預先編譯着色器並打包程序,因為編譯的着色器取決於許多因素,如圖形卡模型、制造商和驅動程序。相反,我們包括着色器源代碼,並在程序開始時編譯它。
我們將通過創建一個着色器類來做到這一點,該類編譯着色器並包裝幾個函數,我們將在稍后看到。
public class Shader { int Handle; public Shader(string vertexPath, string fragmentPath) { } }
句柄將表示我們最終着色器程序在編譯完成后的位置。我們將在構造函數中進行所有初始化。
首先,在構造函數中,定義兩個 int:VertexShader
和FragmentShader
。這些是各個着色器的句柄。它們在構造函數中定義,因為在完整着色器程序完成后,我們不需要單獨的着色器。
接下來,我們需要從各個着色器文件加載源代碼。我們可以像這樣做:
string VertexShaderSource; using (StreamReader reader = new StreamReader(vertexPath, Encoding.UTF8)) { VertexShaderSource = reader.ReadToEnd(); } string FragmentShaderSource; using (StreamReader reader = new StreamReader(fragmentPath, Encoding.UTF8)) { FragmentShaderSource = reader.ReadToEnd(); }
然后,我們生成着色器,並將源代碼綁定到着色器。
VertexShader = GL.CreateShader(ShaderType.VertexShader); GL.ShaderSource(VertexShader, VertexShaderSource); FragmentShader = GL.CreateShader(ShaderType.FragmentShader); GL.ShaderSource(FragmentShader, FragmentShaderSource);
然后,我們編譯着色器並檢查錯誤。
GL.CompileShader(VertexShader); string infoLogVert = GL.GetShaderInfoLog(VertexShader); if (infoLogVert != System.String.Empty) System.Console.WriteLine(infoLogVert); GL.CompileShader(FragmentShader); string infoLogFrag = GL.GetShaderInfoLog(FragmentShader); if (infoLogFrag != System.String.Empty) System.Console.WriteLine(infoLogFrag);
如果在編譯時出現任何錯誤,可以使用函數GL.GetShaderInfoLog
獲取調試字符串。假設沒有問題,我們可以繼續鏈接。
GL.DetachShader(Handle, VertexShader);
GL.DetachShader(Handle, FragmentShader);
GL.DeleteShader(FragmentShader);
GL.DeleteShader(VertexShader);
我們現在有一個有效的着色器,所以讓我們添加一種方法來使用它。將此函數添加Shader
類:
void Use() { GL.UseProgram(Handle); }
最后,我們需要在此類使用完成后清理句柄。由於面向對象語言問題,無法在最終化器中完成。相反,我們必須從 IDisposable
派生,並記住手動調用着色器上的Dispose
。在代碼的其余部分下方添加以下內容:
private bool disposedValue = false; protected virtual void Dispose(bool disposing) { if (!disposedValue) { GL.DeleteProgram(Handle); disposedValue = true; } } ~Shader() { GL.DeleteProgram(Handle); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
祝賀!我們現在有一個功能齊全的着色器類
回到Game
類中,添加新屬性Shader shader
;然后,在OnLoad
中,添加shader = new Shader("shader.vert", "shader.frag");
。然后,轉到 OnUnload
,然后添加行shader.Dispose();
。
嘗試運行;如果沒有打印到控制台, 您的着色器已正確編譯!
鏈接頂點屬性
頂點數組對象
增編:動態檢索着色器布局
原文地址:https://blog.csdn.net/u014786187/article/details/109356789