OpenTK教程-2繪制一個三角形(正確的方式)


上一個教程向我們展示了如何在屏幕上畫一個三角形。但是,我說過,那是一種古老的方式,即使它能夠正常運行,但是現在這已經不是“正確”的方式。上篇文章中我們將幾何發送到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.GetAttribLocationGL.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上找到。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM