【轉】【OpenTK】C# OpenTK教程 著色器 layout
圖形管道
在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