上一个教程向我们展示了如何在屏幕上画一个三角形。但是,我说过,那是一种古老的方式,即使它能够正常运行,但是现在这已经不是 "正确" 的方式。上篇文章中我们将几何发送到 GPU 的方式是所谓的 "即时模式",它非常简单,但是已经不再推荐使用。
在本教程中,我们将要实现同样的最终目标,但是我们将以更复杂的方式来做事情,疯了么大哥?
我们选择更麻烦的编写方式,是为了更有效率,更快速和可扩展性。
我们将像以前的教程一样开始,我将引用原文几次,所以如果还没有看过上一篇的话,请抽空看看。
要开始,我们需要创建一个新的项目,引用 OpenTK 和 System.Drawing,同上一个教程。将其命名为 OpenTKTutorial2。
首先,我们需要再次做一些基础工作,就像第一个教程那样。添加一个名为 "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。在这个函数中,我们将首先调用
函数,该函数返回一个新程序对象的 ID,我们将它存储在 pgmID 中。
- GL.CreateProgram()
- 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
函数。每个都使用着色器中的程序的 ID 和变量的名称。
- GL.GetUniformLocation
在 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://www.cnblogs.com/podolski/p/7416655.html