OpenTK是一个跨平台的包,可以让我们能够使用​C#​来调用​OpenGL​来进行三维可视化的开发。

因为这是第一篇,所以会写的比较详细,后面重复用到的内容就不再二次说明了。

  1. 创建主程序中的类:
using OpenTK.Mathematics;
using OpenTK.Windowing.Desktop;

namespace OpenTK_SelfMadeBasis
{
class Program
{
static void Main(string[] args)
{
// 其中根据c#语言特性,第一个NativeWindowSettings也可以用var替换
NativeWindowSettings nativeWindowSettings = new NativeWindowSettings()
{
// 通常用Vector2i就可以了,设定window尺寸
Size = new Vector2i(800, 600),
Title = "Draw Single Line",
};
// 启动主程序
using (Window window = new Window(GameWindowSettings.Default, nativeWindowSettings))
{
window.Run();
}
}
}
}

其中​​NativeWindowSettings​​​是​​OpenTK.Windowing.Desktop​​​域名下的内容,因此在程序的开头,我们需要使用​​using OpenTK.Windowing.Desktop;​​​来对该域名进行引用。此外,函数​​Vector2i()​​​是​​OpenTK.Mathematics​​​域名下的函数,因此同样在开头我们要使用​​using OpenTK.Mathematics;​​​对该域名进行引用。函数​​Vector2i(x, y)​​​可以定义一个二维向量,我们通过将这个二维向量赋值给​​NativeWindowSettings​​​下的默认​​Size​​​属性可以实现对我们对之后绘图使用的​​GUI​​​窗口大小的调整。同理,给默认​​Title​​​属性传递一个字符串可以实现对绘图使用的​​GUI​​窗口的名称的调整。上述的代码基本可以说是固定的格式,唯一需要改动的也只有窗口的尺寸和名称,其余部分均可保持不变。

  1. 构建主程序中调用的​​GUI​​窗口界面的类
using OpenTK_SelfMadeBasis.Common;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.GraphicsLibraryFramework;
using OpenTK.Windowing.Desktop;

// 使用EBO控制要绘制的点,使用DrawArrays方法绘制单个点,这种方法不需要EBO
namespace OpenTK_SelfMadeBasis
{
// 创建的GUI窗口类是从OpenTK中定义的GameWindow类中继承的
public class Window : GameWindow
{
// 设定所需要绘制点的坐标,因为是3维中的点,因此一个点需要具有三个坐标(x, y, z)
private readonly float[] _vertices =
{
-0.5f, -0.5f, 0.0f,
};

// 创建一个Vertex Buffer Object(VBO)的句柄
private int _vertexBufferObject;

// 创建一个Vertex Array Object(VAO)的句柄
private int _vertexArrayObject;

// 创建一个Shader(着色器)的句柄
private Shader _shader;

// 这里是一个构造函数
public Window(GameWindowSettings gameWindowSettings, NativeWindowSettings nativeWindowSettings)
: base(gameWindowSettings, nativeWindowSettings)
{
}

// OnLoad:OpenGL初始化函数
protected override void OnLoad()
{
// 设定清除屏幕后屏幕显示的背景色,这里是深绿色
GL.ClearColor(0.2f, 0.3f, 0.3f, 1.0f);

// 创建一个缓冲区,GL.GenBuffer()会返回一个句柄,这个句柄在之前我们已经声明过了
_vertexBufferObject = GL.GenBuffer();

// 将创建的缓冲区句柄和目标缓冲区ArrayBuffer绑定起来,之后通过句柄_vertexBufferObject可以实现对ArrayBuffer的操作
GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBufferObject);

// 上传之前定义的端点值到缓冲区
GL.BufferData(BufferTarget.ArrayBuffer, _vertices.Length * sizeof(float), _vertices, BufferUsageHint.StaticDraw);

// Gen的意思是generate(产生),这里生成一个VAO用来告知计算机如何分割和处理之前上传的数据
_vertexArrayObject = GL.GenVertexArray();
// 将VAO与VertextArray进行绑定,以后通过_vertexArrayObject句柄来对VertexArray进行操作
GL.BindVertexArray(_vertexArrayObject);

// 定义Shader(着色器)如何处理VBO数据
GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), 0);
GL.EnableVertexAttribArray(0);

// 初始化着色器
_shader = new Shader("Shaders/shader.vert", "Shaders/shader.frag");

// 激活着色器
_shader.Use();
base.OnLoad();
// 注意:VAO和着色器都是全局的
}

// OnRenderFrame:OpenGL绘制图像函数
protected override void OnRenderFrame(FrameEventArgs e)
{
// 清屏
GL.Clear(ClearBufferMask.ColorBufferBit);

// 调用着色器
_shader.Use();

// 调用VAO
GL.BindVertexArray(_vertexArrayObject);

// 绘制ArrayBuffer中接收到的数据
GL.DrawArrays(PrimitiveType.Points, 0, 1);

// 交换缓冲区,防止程序运行异常
SwapBuffers();

base.OnRenderFrame(e);
}
// OnUpdateFrame: 更新窗口函数
protected override void OnUpdateFrame(FrameEventArgs e)
{
var input = KeyboardState;

// 当按下Esc键后关闭GUI窗口
if (input.IsKeyDown(Keys.Escape))
{
Close();
}

base.OnUpdateFrame(e);
}
// OnResize: OpenGL窗口尺寸调整函数
protected override void OnResize(ResizeEventArgs e)
{
// 当我们调整窗口尺寸的时候,相应的物体坐标也跟着调整
GL.Viewport(0, 0, Size.X, Size.Y);
base.OnResize(e);
}

// OnUnload: OpenGL使用后回收资源函数
protected override void OnUnload()
{
// 取消所有对象的绑定
GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
GL.BindVertexArray(0);
GL.UseProgram(0);

// 删除所有的缓冲资源
GL.DeleteBuffer(_vertexBufferObject);
GL.DeleteVertexArray(_vertexArrayObject);
GL.DeleteProgram(_shader.Handle);

base.OnUnload();
}
}
}

​GameWindow​​​是包含在域名​​OpenTK.Windowing.Desktop​​中的,因此在开头需要对该域名进行引用。

  1. 着色器类
    我们可以看到在上面的窗口类中,我们在开头引用了一个​​​OpenTK_SelfMadeBasis.Common;​​​域名,这个实际上是一个着色器类,因为着色器类对于我们以后的所有工程都是通用的,所以将其存放在原始域名​​.Common​​的新域名下。
using System;
using System.IO;
using System.Collections.Generic;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;


namespace OpenTK_Tutorials.Common
{
// 着色器类
public class Shader
{
// 定义(声明)一个只读的句柄
public readonly int Handle;

// 定义一个只读的字典
private readonly Dictionary<string, int> _uniformLocations;

// 这里介绍如何创建一个着色器
// 着色器是用 GLSL 编写的,这是一种在语义上与 C 非常相似的语言。
public Shader(string vertPath, string fragPath)
{
// 加载端点着色器并编译
var shaderSource = File.ReadAllText(vertPath);

// 创建一个VertexShader类型的着色器
var vertexShader = GL.CreateShader(ShaderType.VertexShader);

// 将GLSL原代码和端点着色器绑定
GL.ShaderSource(vertexShader, shaderSource);

// 编译端点着色器
CompileShader(vertexShader);

// 对片段着色器做同样的事情
shaderSource = File.ReadAllText(fragPath);
var fragmentShader = GL.CreateShader(ShaderType.FragmentShader);
GL.ShaderSource(fragmentShader, shaderSource);
CompileShader(fragmentShader);

// 必须将这两个着色器放入OpenGL能够执行的程序中
// 创建一个程序
Handle = GL.CreateProgram();

// 将两个着色器都与之绑定
GL.AttachShader(Handle, vertexShader);
GL.AttachShader(Handle, fragmentShader);

// 然后将他们两个关联起来
LinkProgram(Handle);

// 当程序被链接并编译以后,着色器相应的代码就被复制到了程序中,单个的着色器就不再被需要了,将着色器与程序分离,并删除他们
GL.DetachShader(Handle, vertexShader);
GL.DetachShader(Handle, fragmentShader);
GL.DeleteShader(fragmentShader);
GL.DeleteShader(vertexShader);

// 获取活动的全局着色器(uniforms)数量
GL.GetProgram(Handle, GetProgramParameterName.ActiveUniforms, out var numberOfUniforms);

// 分配字典来保存位置
_uniformLocations = new Dictionary<string, int>();

// 遍历所有全局着色器
for (var i = 0; i < numberOfUniforms; i++)
{
// 获取全局着色器的名字
var key = GL.GetActiveUniform(Handle, i, out _, out _);

// 获取位置
var location = GL.GetUniformLocation(Handle, key);

// 将它添加到字典中
_uniformLocations.Add(key, location);
}
}

private static void CompileShader(int shader)
{
// 编译着色器
GL.CompileShader(shader);

// 检查是否存在编译错误
GL.GetShader(shader, ShaderParameter.CompileStatus, out var code);
if (code != (int)All.True)
{
// 使用GL.GetShaderInfoLog(shader)函数来获取作物的详细信息
var infoLog = GL.GetShaderInfoLog(shader);
throw new Exception($"Error occurred whilst compiling Shader({shader}).\n\n{infoLog}");
}
}

private static void LinkProgram(int program)
{
// 链接程序
GL.LinkProgram(program);

// 检查错误
GL.GetProgram(program, GetProgramParameterName.LinkStatus, out var code);
if (code != (int)All.True)
{
throw new Exception($"Error occurred whilst linking Program({program})");
}
}

// 启动着色器程序的封装函数
public void Use()
{
GL.UseProgram(Handle);
}

public int GetAttribLocation(string attribName)
{
return GL.GetAttribLocation(Handle, attribName);
}

public void SetInt(string name, int data)
{
GL.UseProgram(Handle);
GL.Uniform1(_uniformLocations[name], data);
}

public void SetFloat(string name, float data)
{
GL.UseProgram(Handle);
GL.Uniform1(_uniformLocations[name], data);
}

public void SetMatrix4(string name, Matrix4 data)
{
GL.UseProgram(Handle);
GL.UniformMatrix4(_uniformLocations[name], true, ref data);
}

public void SetVector3(string name, Vector3 data)
{
GL.UseProgram(Handle);
GL.Uniform3(_uniformLocations[name], data);
}
}
}

下面介绍端点着色器和片段着色器的​​GLSL​​写法。

  1. 端点着色器
// 告诉应该使用哪个版本的GLSL编译器
#version 330

// in 表示输入量
// vec3表示是一个三维向量
// aPosition是这个三维向量的名称
layout(location = 0) in vec3 aPosition;

// 将三维端点值传入即可,1.0表示另一个高级参数,这里不做过多解释,取1.0,也可以省略
void main(void)
{
gl_Position = vec4(aPosition, 1.0);
}
  1. 片段着色器
#version 330

// 声明一个输出的四维向量outputColor
out vec4 outputColor;

void main()
{
// 设定输出的颜色,分别对应RGBA值
outputColor = vec4(1.0, 1.0, 0.0, 1.0);
}

我们可以看到上面使用了许多句柄,所谓句柄,简单地说就是一个变量,通过这个变量可以实现对特定对象的访问。

对于端点和片段着色器,我们需要将它们放在​​C:\....\OpenTK_SelfMadeBasis\DrawSinglePoints\bin\Debug\netcoreapp3.1\Shaders​​​目录下才行,其中​​C:\....\OpenTK_SelfMadeBasis\DrawSinglePoints​​表示我们创建的工程目录,并且这两个着色器的后缀名如下图所示。

OpenTK---空间中单个三维点的绘制_c#


注意:上述着色器代码中的注释是中文给出的,是为了方便大家理解着色器代码,当大家复制代码后,应该删除掉中文注释,着色器内部的代码不能用中文注释,否则会报错,但是可以使用英文进行注释。运行程序可以得到下面的结果:

OpenTK---空间中单个三维点的绘制_c#_02

至此,我们就完成了单个三维点在空间中的绘图表示。重要是通过这个示例来展示基础的​​OpenTK​​操作框架。之后我们会进行真实的三维图形的绘制。

关于上述的四个类文件如何配置,请看这篇​​Visual Studio内配置OpenTK环境​​(超链接点击跳转)。