上一個教程向咱們展現瞭如何在屏幕上畫一個三角形。可是,我說過,那是一種古老的方式,即便它可以正常運行,可是如今這已經不是「正確」的方式。上篇文章中咱們將幾何發送到GPU的方式是所謂的「即時模式」,它很是簡單,可是已經再也不推薦使用。html
在本教程中,咱們將要實現一樣的最終目標,可是咱們將以更復雜的方式來作事情,瘋了麼大哥?git
咱們選擇更麻煩的編寫方式,是爲了更有效率,更快速和可擴展性。github
咱們將像之前的教程同樣開始,我將引用原文幾回,因此若是尚未看過上一篇的話,請抽空看看。c#
要開始,咱們須要建立一個新的項目,引用OpenTK和System.Drawing,同上一個教程。將其命名爲OpenTKTutorial2。數組
首先,咱們須要再次作一些基礎工做,就像第一個教程那樣。添加一個名爲「Game」的新類。使它成爲GameWindow的子類(您須要爲OpenTK添加一個using指令才能使用該類)。併發
差很少是這樣:ide
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(); }
好了,從這裏開始,咱們能夠學點新的東西了!spa
咱們首先須要作的是建立咱們的着色器(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上找到。