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

本文详细介绍了如何使用C#和OpenTK库创建一个简单的三维点绘制程序。首先创建主程序设置窗口大小和标题,然后通过GameWindow类构建GUI窗口,并在OnLoad和OnRenderFrame方法中初始化OpenGL,设置背景色,创建顶点缓冲对象和顶点数组对象,以及加载和使用着色器。最后,展示了着色器的GLSL代码。整个过程为后续的三维图形绘制奠定了基础。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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();
            }
        }
    }
}

其中NativeWindowSettingsOpenTK.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 core

// 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操作框架。之后我们会进行真实的三维图形的绘制。

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

码字不易,如果大家觉得有用,请高抬贵手给一个赞让我上推荐让更多的人看到吧~

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

勤奋的大熊猫

你的鼓励将是我写作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值