之前我们已经提及了如何在空间中绘制单条线段,这里,我们将会说到如何在空间中绘制三条线段,使它们相互组合看起来像是一个三角形。注意,这里实际上我们所画的并不是一个三角形,因为三角形通常要求三条线段所围成的部分被填充。

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

namespace OpenTK_SelfMadeBasis
{
class Program
{
static void Main(string[] args)
{
NativeWindowSettings nativeWindowSettings = new NativeWindowSettings()
{
Size = new Vector2i(800, 600),
Title = "Draw Three Lines",
};
using (Window window = new Window(GameWindowSettings.Default, nativeWindowSettings))
{
window.Run();
}
}
}
}

截至到这篇为止,我们可以看到,无论我们想要在空间中画什么样的图形,主程序中的类都拥有相同的形式。唯一的差异是我们重新定义了​​Title​​​参数的值,现在是​​Draw Three Lines​​。

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

namespace OpenTK_SelfMadeBasis
{
public class Window : GameWindow
{
// 我们想要画三条线段,并使其能够围成一个三角形,那么我们需要三个端点坐标
private readonly float[] _vertices =
{
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f,
};

// uint类型表示仅仅为单个的一个数,常作为placeholder(位置占用符)用
// 三个点,通过第0个点和第2个点绘制第1条线段,通过第0个点和第2个点绘制第2条线段,通过第1个点和第2个点绘制第3条线段
private readonly uint[] _indices =
{
0, 1,
0, 2,
1, 2,
};

private int _vertexBufferObject;

private int _vertexArrayObject;

private Shader _shader;

private int _elementBufferObject; // EBO,主要用于控制绘制三条线段的顺序

public Window(GameWindowSettings gameWindowSettings, NativeWindowSettings nativeWindowSettings)
: base(gameWindowSettings, nativeWindowSettings)
{
}

protected override void OnLoad()
{
GL.ClearColor(0.2f, 0.3f, 0.3f, 1.0f);

_vertexBufferObject = GL.GenBuffer();

GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBufferObject);

GL.BufferData(BufferTarget.ArrayBuffer, _vertices.Length * sizeof(float), _vertices, BufferUsageHint.StaticDraw);

_vertexArrayObject = GL.GenVertexArray();
GL.BindVertexArray(_vertexArrayObject);

GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), 0);
GL.EnableVertexAttribArray(0);

_elementBufferObject = GL.GenBuffer(); // VBO
GL.BindBuffer(BufferTarget.ElementArrayBuffer, _elementBufferObject);
GL.BufferData(BufferTarget.ElementArrayBuffer, _indices.Length * sizeof(uint), _indices, BufferUsageHint.StaticDraw);

_shader = new Shader("Shaders/shader.vert", "Shaders/shader.frag");

_shader.Use();

base.OnLoad();
}

protected override void OnRenderFrame(FrameEventArgs e)
{
GL.Clear(ClearBufferMask.ColorBufferBit);

_shader.Use();

GL.BindVertexArray(_vertexArrayObject);

GL.DrawElements(PrimitiveType.Lines, _indices.Length, DrawElementsType.UnsignedInt, 0);

SwapBuffers();

base.OnRenderFrame(e);
}

protected override void OnUpdateFrame(FrameEventArgs e)
{
var input = KeyboardState;

if (input.IsKeyDown(Keys.Escape))
{
Close();
}

base.OnUpdateFrame(e);
}

protected override void OnResize(ResizeEventArgs e)
{
GL.Viewport(0, 0, Size.X, Size.Y);
base.OnResize(e);
}

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

细心的小伙伴也许发现了,在这个例子中我们使用的绘制图形命令为​​GL.DrawElements(PrimitiveType.Lines, _indices.Length, DrawElementsType.UnsignedInt, 0);​​​这里介绍一下​​GL.DrawElements()​​函数的用法。

public static void DrawElements(PrimitiveType mode, int count, DrawElementsType type, int indices);

各个参数的意义:
​​​PrimitiveType mode​​​:之前我们已经说到过了,说明要渲染图元的类型。
​​​int count​​​:说明需要被渲染的元素的个数。
​​​DrawElementsType type​​​:说明索引参数的类型,只能为:​​UnsignedByte​​​,​​UnsignedShort​​​或者​​UnsignedInt​​​。本例中我们定义的时候为​​UnsignedInt​​​类型。
​​​int indices​​​:说明索引参数存放的开始位置,将指针指向该处。我们这里的​​_indices​​​参数中索引参数从第一个就开始了,因此这个值应该设置为​​0​​。

那么这个函数与我们之前提到的​​GL.DrawArrays()​​有什么区别呢?并且两个函数各自的应用场景又是什么呢?

个人理解:如果每个端点只使用一次,并且端点之间是逐个按次序连接的关系,那么可以使用​​DrawArrays()​​​方法。相反如果端点需要被重复使用,则应使用​​DrawElements()​​方法。即,单次有序调用使用DrawArrays()方法,多次无序调用使用​DrawElements()​方法

  1. 着色器类
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;

namespace OpenTK_SelfMadeBasis.Common
{
public class Shader
{
public readonly int Handle;

private readonly Dictionary<string, int> _uniformLocations;

public Shader(string vertPath, string fragPath)
{
var shaderSource = File.ReadAllText(vertPath);

var vertexShader = GL.CreateShader(ShaderType.VertexShader);

GL.ShaderSource(vertexShader, shaderSource);

CompileShader(vertexShader);

shaderSource = File.ReadAllText(fragPath);
var fragmentShader = GL.CreateShader(ShaderType.FragmentShader);
GL.ShaderSource(fragmentShader, shaderSource);
CompileShader(fragmentShader);

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

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

仔细对比不难发现,我们的着色器类也与前面提及的着色器类别无二致。

  1. 端点着色器
#version 330 core

layout(location = 0) in vec3 aPosition;

void main(void)
{
gl_Position = vec4(aPosition, 1.0);
}
  1. 片段着色器
#version 330

out vec4 outputColor;

void main()
{
outputColor = vec4(1.0, 1.0, 0.0, 1.0);
}

仔细对比不难发现,我们​​GLSL​​写的端点和片段着色器与之前绘制单条直线时所使用的着色器写法一致。

运行程序可以得到下面的结果:

OpenTK---空间中三条线段的绘制_desktop

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