上一個教程向我們展示了如何在屏幕上畫一個三角形。但是,我說過,那是一種古老的方式,即使它能夠正常運行,但是現在這已經不是“正確”的方式。上篇文章中我們將幾何發送到GPU的方式是所謂的“即時模式”,它非常簡單,但是已經不再推薦使用。
在本教程中,我們將要實現同樣的最終目標,但是我們將以更復雜的方式來做事情,瘋了么大哥?
我們選擇更麻煩的編寫方式,是為了更有效率,更快速和可擴展性。
我們將像以前的教程一樣開始,我將引用原文幾次,所以如果還沒有看過上一篇的話,請抽空看看。
Part 1:設置
要開始,我們需要創建一個新的項目,引用OpenTK和System.Drawing,同上一個教程。將其命名為OpenTKTutorial2。
Part 2:編碼
首先,我們需要再次做一些基礎工作,就像第一個教程那樣。添加一個名為“Game”的新類。使它成為GameWindow的子類(您需要為OpenTK添加一個using指令才能使用該類)。
差不多是這樣:
using OpenTK;
namespace OpentkTutorials2
{
class Game : GameWindow
{
}
}
回到Program.cs,添加代碼:
namespace OpentkTutorials2
{
class Program
{
static void Main(string[] args)
{
using (var game = new Game())
{
game.Run(30.0);
}
}
}
}
Onload方法和OnRenderFrame方法參照上一個教程做就行了。
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
//修改窗口標題
Title = "Hello OpenTK!";
//設置背景顏色為,額,不知道什么藍(需要添加 OpenTK.Graphics.OpenGL and System.Drawing引用)
GL.ClearColor(Color.CornflowerBlue);
}
protected override void OnRenderFrame(FrameEventArgs e)
{
base.OnRenderFrame(e);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
SwapBuffers();
}
好了,從這里開始,我們可以學點新的東西了!
我們首先需要做的是創建我們的着色器(Shader)。現代OpenGL使用它獲知如何繪制給出的值。我們將使用兩種着色器:頂點着色器(Vertex Shader)和片段着色器(Fragment Shader)。 頂點着色器告訴顯卡正在繪制的形狀中的點的信息。片段着色器決定繪制到屏幕時形狀的每個像素的顏色。我們將要使用的代碼非常簡單,但是我們可以使用類似於即時模式的風格操作。OpenGL的着色器以類C語言的腳本語言編寫,稱為GLSL(DirectX使用稍微不同的語言,稱為HLSL)。
譯者注:有另外一篇非常好的文章講GLSL,推薦先閱讀以更深入了解GLSL:LearnOpenGL CN,該系列教程也非常推薦閱讀。
將一個文本文件添加到您的項目中,名為“vs.glsl”。 這將存儲我們的頂點着色器:
#version 330
in vec3 vPosition;
in vec3 vColor;
out vec4 color;
uniform mat4 modelview;
void
main()
{
gl_Position = modelview * vec4(vPosition, 1.0);
color = vec4( vColor, 1.0);
}
注意:對於着色器文件,您可能需要告訴IDE將其復制到輸出目錄(設置文件為始終復制),否則程序將無法找到它們!
第一行告訴鏈接器正在使用哪個版本的GLSL。
“in”行表示每個頂點具有的不同變量。“out”變量被發送到圖形流水線的下一部分,在其中進行插值,以便跨片段平滑過渡。我們通常發送每個頂點的顏色。 “vec3”類型是指具有三個值的向量,“vec4”是具有四個值的向量。
這里還有一個“uniform”變量,對於整個被繪制的對象來說,該變量是相同的。 這將有我們的轉換矩陣,所以我們可以一次性改變對象中的頂點。我們還沒有用到它,但我們很快就會使用它的。
我們的片段着色器更簡單。 將以下內容另存為“fs.glsl”:
#version 330
in vec4 color;
out vec4 outputColor;
void
main()
{
outputColor = color;
}
它只是獲得上一個着色器輸出的顏色變量(注意它現在是“輸入”的“in”),並將輸出設置為該顏色。
現在我們有了這些着色器,接下來我們需要指示顯卡去使用它們。首先,我們需要告訴OpenTK創建一個新的程序對象(program)。 它將以可用的形式存儲的這些着色器。
首先,定義程序的ID(它的地址)變量,置於其他函數之外。我們在代碼中不存儲程序對象本身,而是存儲一個可以引用的地址,程序其本身將存儲在顯卡中。
int pgmID;
在Game類中創建一個新的函數,稱為initProgram。在這個函數中,我們將首先調用GL.CreateProgram()函數,該函數返回一個新程序對象的ID,我們將它存儲在pgmID中。
void initProgram()
{
pgmID = GL.CreateProgram();
}
然后我們需要寫一個加載器來讀取我們的着色器代碼並添加它們。此函數需要獲取文件名和一些信息,並返回創建的着色器的地址。
它應該看起來像這樣:
void loadShader(String filename,ShaderType type, int program, out int address)
{
address = GL.CreateShader(type);
using (StreamReader sr = new StreamReader(filename))
{
GL.ShaderSource(address, sr.ReadToEnd());
}
GL.CompileShader(address);
GL.AttachShader(program, address);
Console.WriteLine(GL.GetShaderInfoLog(address));
}
上面代碼將創建一個新的着色器(使用ShaderType枚舉中的值),為其加載代碼,編譯,並將其添加到我們的程序中。它還會在控制台中將發現的任何錯誤打印出來,當在着色器中發生錯誤時,這是非常好的(如果您使用過時的代碼,它也會警告)。
現在我們有了這個,我們來添加我們的着色器。首先我們在類上定義兩個變量:
int vsID;
int fsID;
這些將存儲我們兩個着色器的地址。 現在,我們要使用我們從文件中加載着色器的功能。
將以下代碼添加到initProgram中:
loadShader("vs.glsl", ShaderType.VertexShader, pgmID, out vsID);
loadShader("fs.glsl", ShaderType.FragmentShader, pgmID, out fsID);
現在,添加了着色器,程序需要鏈接。像C代碼一樣,代碼首先被編譯,然后被鏈接,完成從人類可讀的代碼到需要的機器語言的轉變。
然后再添加:
GL.LinkProgram(pgmID);
Console.WriteLine(GL.GetProgramInfoLog(pgmID));
這將鏈接它,並告訴我們是否有錯誤。
着色器現在被添加到我們的程序中,但是我們需要告訴程序更多的信息才能正常工作。我們在我們的頂點着色器上有多個輸入,所以我們需要告訴它們地址來給出頂點的着色器位置和顏色信息。
將此代碼添加到Game類中:
int attribute_vcol;
int attribute_vpos;
int uniform_mview;
我們在這里定義三個變量,存儲每個變量的位置,以供將來引用。往后我們將需要使用這些值,所以我們應該保持簡單。要獲取每個變量的地址,我們使用GL.GetAttribLocation和GL.GetUniformLocation函數。每個都使用着色器中的程序的ID和變量的名稱。
在initProgram結尾處添加:
attribute_vpos = GL.GetAttribLocation(pgmID, "vPosition");
attribute_vcol = GL.GetAttribLocation(pgmID, "vColor");
uniform_mview = GL.GetUniformLocation(pgmID, "modelview");
if (attribute_vpos == -1 || attribute_vcol == -1 || uniform_mview == -1)
{
Console.WriteLine("Error binding attributes");
}
上面代碼將存儲我們需要的值,並且還要做一個簡單的檢查,以確保找到屬性。
譯者注:也可以不在C#代碼中指定,而在shader代碼中使用layout (location = x)的方式指定。具體用法可以參見上文中說的
現在我們的着色器和程序已經建立起來了,但是我們還需要給他們一些東西繪制。為此,我們將使用頂點緩沖區對象(VBO)。 當您使用VBO時,首先需要讓顯卡創建一個,然后綁定到它並發送你的信息。最后,當DrawArrays函數被調用時,緩沖區中的信息將被一次性發送到着色器並繪制到屏幕上。
像着色器的變量一樣,我們也需要存儲地址以供將來使用:
int vbo_position;
int vbo_color;
int vbo_mview;
創建緩沖區非常簡單。在initProgram中添加:
GL.GenBuffers(1, out vbo_position);
GL.GenBuffers(1, out vbo_color);
GL.GenBuffers(1, out vbo_mview);
這將生成3個單獨的緩沖區並將其地址存儲在我們的變量中。對於像這樣的多個緩沖區,有一個可以生成多個緩沖區並將它們存儲在數組中的選項,但是為了簡單起見,在這里我們將它們保留在單獨的int中。
這些緩沖區將需要一些數據。位置和顏色都為Vector3類型,模型視圖為Matrix4類型。我們需要將它們存儲在一個數組中,這樣可以更有效地將數據發送到緩沖區。
向Game類添加三個變量:
Vector3[] vertdata;
Vector3[] coldata;
Matrix4[] mviewdata;
這個例子中,我們將在onLoad中設置這些值,並調用initProgram():
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
initProgram();
vertdata = new Vector3[] { new Vector3(-0.8f, -0.8f, 0f),
new Vector3( 0.8f, -0.8f, 0f),
new Vector3( 0f, 0.8f, 0f)};
coldata = new Vector3[] { new Vector3(1f, 0f, 0f),
new Vector3( 0f, 0f, 1f),
new Vector3( 0f, 1f, 0f)};
mviewdata = new Matrix4[]{
Matrix4.Identity
};
Title = "Hello OpenTK!";
GL.ClearColor(Color.CornflowerBlue);
GL.PointSize(5f);
}
數據存儲完畢,我們就可以發送到緩沖區了。我們需要為OnUpdateFrame函數添加另一個重載。首先是綁定到緩沖區:
GL.BindBuffer(BufferTarget.ArrayBuffer, vbo_position);
這就告訴OpenTK,如果我們發送任何數據,我們將使用該緩沖區。接下來,我們會發送數據:
GL.BufferData<Vector3>(BufferTarget.ArrayBuffer, (IntPtr)(vertdata.Length * Vector3.SizeInBytes), vertdata, BufferUsageHint.StaticDraw);
這段代碼告訴我們,我們發送的長度為(vertdata.Length * Vector3.SizeInBytes)的vertdata到緩沖區。最后,我們需要告訴它使用這個緩沖區(最后一個綁定到)vPosition變量,這將需要3個float值:
GL.VertexAttribPointer(attribute_vpos, 3, VertexAttribPointerType.Float, false, 0, 0);
所以,最后合起來:
GL.BindBuffer(BufferTarget.ArrayBuffer, vbo_position);
GL.BufferData<Vector3>(BufferTarget.ArrayBuffer, (IntPtr)(vertdata.Length * Vector3.SizeInBytes), vertdata, BufferUsageHint.StaticDraw);
GL.VertexAttribPointer(attribute_vpos, 3, VertexAttribPointerType.Float, false, 0, 0);
GL.BindBuffer(BufferTarget.ArrayBuffer, vbo_color);
GL.BufferData<Vector3>(BufferTarget.ArrayBuffer, (IntPtr)(coldata.Length * Vector3.SizeInBytes), coldata, BufferUsageHint.StaticDraw);
GL.VertexAttribPointer(attribute_vcol, 3, VertexAttribPointerType.Float, true, 0, 0);
我們還需要發送模型視圖矩陣(Model-View Matrix):
GL.UniformMatrix4(uniform_mview, false, ref mviewdata[0]);
最后,我們要清除緩沖區綁定,並將其設置為與我們的着色器一起使用該程序:
GL.UseProgram(pgmID);
GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
快要大功告成了! 現在我們將數據、着色器發送到顯卡,但是我們還需要繪制他們。在我們的OnRenderFrame函數中,首先我們需要告訴它使用我們想要的變量:
GL.EnableVertexAttribArray(attribute_vpos);
GL.EnableVertexAttribArray(attribute_vcol);
然后我們告訴它如何繪制它們:
GL.DrawArrays(PrimitiveType.Triangles, 0, 3);
最后是清理工作:
GL.DisableVertexAttribArray(attribute_vpos);
GL.DisableVertexAttribArray(attribute_vcol);
GL.Flush();
最終看起來是這樣子:
protected override void OnRenderFrame(FrameEventArgs e)
{
base.OnRenderFrame(e);
GL.Viewport(0, 0, Width, Height);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
GL.Enable(EnableCap.DepthTest);<
GL.EnableVertexAttribArray(attribute_vpos);
GL.EnableVertexAttribArray(attribute_vcol);
GL.DrawArrays(BeginMode.Triangles, 0, 3);
GL.DisableVertexAttribArray(attribute_vpos);
GL.DisableVertexAttribArray(attribute_vcol);
GL.Flush();
SwapBuffers();
}
如果你運行這些代碼,效果是不是很熟悉?

本系列教程翻譯自Neo Kabuto's Blog。已經取得作者授權。
本文原文地址http://neokabuto.blogspot.com/2013/03/opentk-tutorial-2-drawing-triangle.html
原文代碼可以在github上找到。
