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:,该系列教程也⾮常推荐阅读。
将⼀个⽂本⽂件添加到您的项⽬中,名为“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
writeline教程};
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();
}
如果你运⾏这些代码,效果是不是很熟悉?
本系列教程翻译⾃。已经取得作者授权。
原⽂代码可以在上到。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。