OpenTK教程-2绘制一个三角形(正确的方式)

上一个教程向咱们展现了如何在屏幕上画一个三角形。可是,我说过,那是一种古老的方式,即便它可以正常运行,可是如今这已经不是“正确”的方式。上篇文章中咱们将几何发送到GPU的方式是所谓的“即时模式”,它很是简单,可是已经再也不推荐使用。html

在本教程中,咱们将要实现一样的最终目标,可是咱们将以更复杂的方式来作事情,疯了么大哥?git

咱们选择更麻烦的编写方式,是为了更有效率,更快速和可扩展性。github

咱们将像之前的教程同样开始,我将引用原文几回,因此若是尚未看过上一篇的话,请抽空看看。c#

Part 1:设置

要开始,咱们须要建立一个新的项目,引用OpenTK和System.Drawing,同上一个教程。将其命名为OpenTKTutorial2。数组

Part 2:编码

首先,咱们须要再次作一些基础工做,就像第一个教程那样。添加一个名为“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.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上找到。

相关文章
相关标签/搜索