OpenTK
是一个跨平台的包,可以让我们能够使用C#
来调用OpenGL
来进行三维可视化的开发。
因为这是第一篇,所以会写的比较详细,后面重复用到的内容就不再二次说明了。
- 创建主程序中的类:
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
窗口的名称的调整。上述的代码基本可以说是固定的格式,唯一需要改动的也只有窗口的尺寸和名称,其余部分均可保持不变。
- 构建主程序中调用的
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
中的,因此在开头需要对该域名进行引用。
- 着色器类
我们可以看到在上面的窗口类中,我们在开头引用了一个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
写法。
- 端点着色器
// 告诉应该使用哪个版本的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);
}
- 片段着色器
#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
操作框架。之后我们会进行真实的三维图形的绘制。
关于上述的四个类文件如何配置,请看这篇Visual Studio内配置OpenTK环境(超链接点击跳转)。