I have been looking all around for OpenTK tutorials however I could not find any.
From a bunch of project on GitHub I have managed to make the following structure (Window, Shader Loading, Wavefront model importer, etc.), however I can’t find how to actually render the imported 3D model. I’m sorry for a really request, however I am just getting into OpenTK and I have how everything works, just trying to get an example working right now.
I was expecting to use the normals
, texCoords
and indices
lists somewhere, however I have no Idea where I should do that.
NOTE: Material class just containts an instance of “Shader”
Mesh.cs
using OpenTK.Mathematics;
using System.Globalization;
namespace Game.Engine.Graphics
{
public class Mesh
{
public Material material;
public Vector3 position;
public Vector3 rotation;
public float scale;
private List<Vector3> vertices = new List<Vector3>();
private List<Vector3> normals = new List<Vector3>();
private List<Vector2> texCoords = new List<Vector2>();
private List<Tuple<int, int, int>> faces = new List<Tuple<int, int, int>>();
private List<int> indices = new List<int>();
private VertexArrayObject vertexArray;
private VertexBufferObject vertexBuffer;
public Mesh(Material material, Vector3 position, Vector3 rotation, float scale = 1f)
{
this.material = material;
this.position = position;
this.rotation = rotation;
}
public void Render(Camera camera)
{
Matrix4 model = Matrix4.CreateScale(scale) * Matrix4.CreateRotationX(rotation.X) * Matrix4.CreateRotationY(rotation.Y) * Matrix4.CreateRotationZ(rotation.Z) * Matrix4.CreateTranslation(position);
Matrix4 view = camera.GetViewMatrix();
Matrix4 projection = camera.GetProjectionMatrix();
material.shader.SetMatrix4("model", ref model);
material.shader.SetMatrix4("view", ref view);
material.shader.SetMatrix4("projection", ref projection);
GL.DrawElements(PrimitiveType.Triangles, indices.Count, DrawElementsType.UnsignedInt, indices.ToArray());
}
public void ImportFromFile(string fileName)
{
try
{
using (StreamReader reader = new StreamReader("../../../Assets/Meshes/" + fileName))
{
string line;
while ((line = reader.ReadLine()) != null)
{
string[] parameters = line.Split(" ");
switch (parameters[0])
{
case "p": // Point
break;
case "v": // Vertex
float x = float.Parse(parameters[1], CultureInfo.InvariantCulture.NumberFormat);
float y = float.Parse(parameters[2], CultureInfo.InvariantCulture.NumberFormat);
float z = float.Parse(parameters[3], CultureInfo.InvariantCulture.NumberFormat);
vertices.Add(new Vector3(x, y, z));
break;
case "vn": // Normal
float nx = float.Parse(parameters[1], CultureInfo.InvariantCulture.NumberFormat);
float ny = float.Parse(parameters[2], CultureInfo.InvariantCulture.NumberFormat);
float nz = float.Parse(parameters[3], CultureInfo.InvariantCulture.NumberFormat);
normals.Add(new Vector3(nx, ny, nz));
break;
case "vt": // TexCoord
float u = float.Parse(parameters[1], CultureInfo.InvariantCulture.NumberFormat);
float v = float.Parse(parameters[2], CultureInfo.InvariantCulture.NumberFormat);
texCoords.Add(new Vector2(u, v));
break;
case "f": // Face
string[] faceParameters = parameters[1].Split("/");
faces.Add(new Tuple<int, int, int>(int.Parse(faceParameters[0]), int.Parse(faceParameters[1]), int.Parse(faceParameters[2])));
break;
}
}
reader.Close();
}
CalculateIndices();
CreateVertexObjects();
Console.WriteLine("Imported mesh from {0} -> Vertices: {1}, Normals: {2}, TexCoords: {3}, Faces: {4}, Indices: {5}", fileName, vertices.Count, normals.Count, texCoords.Count, faces.Count, indices.Count);
}
catch (Exception ex)
{
Console.WriteLine("Failed to import mesh from file: {0}", ex.Message);
}
}
private void CalculateIndices(int offset = 0)
{
indices = new List<int>();
foreach (var face in faces)
{
indices.Add(face.Item1 + offset);
indices.Add(face.Item2 + offset);
indices.Add(face.Item3 + offset);
}
}
private void CreateVertexObjects()
{
vertexArray = new VertexArrayObject();
vertexBuffer = new VertexBufferObject(vertices);
vertexArray.LinkBuffer(0, 3, vertexBuffer);
}
}
}
Camera.cs
using OpenTK.Mathematics;
namespace Game.Engine.Graphics
{
public class Camera
{
public Vector3 position;
public float fieldOfView;
private float aspectRatio;
private float pitch;
private float yaw;
private Vector3 up;
private Vector3 front;
private Vector3 right;
public Camera(Vector3 position, float fieldOfView)
{
this.position = position;
this.fieldOfView = fieldOfView;
yaw = -90f;
}
public void UpdateTransform()
{
pitch = Math.Clamp(pitch, -90f, 90f);
yaw = Math.Clamp(yaw, -90f, 90f);
float pitchRad = MathHelper.DegreesToRadians(pitch);
float yawRad = MathHelper.DegreesToRadians(yaw);
float pitchCos = MathF.Cos(pitchRad);
front.X = pitchCos * MathF.Cos(yawRad);
front.Y = MathF.Sin(pitchRad);
front.Z = pitchCos * MathF.Sin(yawRad);
front.Normalize();
right = Vector3.Normalize(Vector3.Cross(front, Vector3.UnitY));
up = Vector3.Normalize(Vector3.Cross(right, front));
}
public Matrix4 GetViewMatrix()
{
return Matrix4.LookAt(position, position + front, up);
}
public Matrix4 GetProjectionMatrix()
{
return Matrix4.CreatePerspectiveFieldOfView(MathHelper.DegreesToRadians(fieldOfView), aspectRatio, 0.1f, 1000f);
}
public void UpdateAspectRatio(float aspectRatio)
{
this.aspectRatio = aspectRatio;
}
}
}
Shader.cs
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;
namespace Game.Engine.Graphics
{
public class Shader
{
public int ID;
public Shader(string vertexShaderFileName, string fragmentShaderFileName)
{
ID = GL.CreateProgram();
int vertexShader = GL.CreateShader(ShaderType.VertexShader);
GL.ShaderSource(vertexShader, ReadShaderFile(vertexShaderFileName));
GL.CompileShader(vertexShader);
int fragmentShader = GL.CreateShader(ShaderType.FragmentShader);
GL.ShaderSource(fragmentShader, ReadShaderFile(fragmentShaderFileName));
GL.CompileShader(fragmentShader);
GL.AttachShader(ID, vertexShader);
GL.AttachShader(ID, fragmentShader);
GL.LinkProgram(ID);
GL.DeleteShader(vertexShader);
GL.DeleteShader(fragmentShader);
}
public void SetMatrix4(string name, ref Matrix4 data)
{
Bind();
GL.UniformMatrix4(GL.GetUniformLocation(ID, name), true, ref data);
Unbind();
}
public void Bind()
{
GL.UseProgram(ID);
}
public void Dispose()
{
GL.DeleteProgram(ID);
}
public static void Unbind()
{
GL.UseProgram(0);
}
private static string ReadShaderFile(string fileName)
{
string shader = "";
try
{
using (StreamReader reader = new StreamReader("../../../Assets/Shaders/" + fileName))
{
shader = reader.ReadToEnd();
reader.Close();
}
}
catch (Exception ex)
{
Console.WriteLine("Failed to read shader source: {0}", ex.Message);
}
return shader;
}
}
}
VertexArrayObject.cs
using OpenTK.Graphics.OpenGL4;
namespace Game.Engine.Graphics
{
public class VertexArrayObject
{
private int ID;
public VertexArrayObject()
{
ID = GL.GenVertexArray();
}
public void LinkBuffer(int location, int size, VertexBufferObject vbo)
{
Bind();
vbo.Bind();
GL.VertexAttribPointer(location, size, VertexAttribPointerType.Float, false, 0, 0);
GL.EnableVertexAttribArray(location);
Unbind();
}
public void Bind()
{
GL.BindVertexArray(ID);
}
public static void Unbind()
{
GL.BindVertexArray(0);
}
public void Dispose()
{
GL.DeleteVertexArray(ID);
}
}
}
VertexBufferObject.cs
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;
namespace Game.Engine.Graphics
{
public class VertexBufferObject
{
private int ID;
public VertexBufferObject(List<Vector3> data)
{
ID = GL.GenBuffer();
Bind();
GL.BufferData(BufferTarget.ArrayBuffer, data.Count * Vector3.SizeInBytes, data.ToArray(), BufferUsageHint.StaticDraw);
Unbind();
}
public void Bind()
{
GL.BindBuffer(BufferTarget.ArrayBuffer, ID);
}
public static void Unbind()
{
GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
}
public void Dispose()
{
GL.DeleteBuffer(ID);
}
}
}
Window.cs
using OpenTK.Windowing.Desktop;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.GraphicsLibraryFramework;
using Game.Engine.API;
using Game.Engine.Graphics;
namespace Game.Engine
{
public class Window : GameWindow
{
public Scene activeScene;
public Window(string title, Scene scene) : base(GameWindowSettings.Default, new NativeWindowSettings())
{
activeScene = scene;
Title = title;
Size = (720, 500);
CenterWindow();
}
protected override void OnLoad()
{
base.OnLoad();
GL.ClearColor(0.2f, 0.3f, 0.3f, 1.0f);
}
protected override void OnUpdateFrame(FrameEventArgs args)
{
base.OnUpdateFrame(args);
KeyboardState input = KeyboardState;
MouseState mouse = MouseState;
Time.Delta = (float)args.Time;
Input.MousePosition = mouse.Position;
Input.MouseDelta = mouse.Delta;
Input.MouseScrollDelta = mouse.ScrollDelta;
activeScene.camera.UpdateTransform();
}
protected override void OnRenderFrame(FrameEventArgs args)
{
base.OnRenderFrame(args);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
foreach (Mesh mesh in activeScene.meshesToRender)
mesh.Render(activeScene.camera);
Context.SwapBuffers();
}
protected override void OnResize(ResizeEventArgs e)
{
base.OnResize(e);
activeScene.camera.UpdateAspectRatio(e.Width / e.Height);
}
}
}
Program.cs
using Game.Engine;
using Game.Engine.API;
using Game.Engine.Graphics;
using OpenTK.Mathematics;
namespace Game
{
internal class Program
{
static void Main(string[] args)
{
Console.Title = "Game Engine Console";
Scene playground = new Scene();
playground.camera.position = new Vector3(0, 0, 0);
Window gameWindow = new Window("Game", playground);
Shader defaultShader = new Shader("Default.vert.shader", "Default.frag.shader");
Material defaultMaterial = new Material(defaultShader);
Mesh plane = new Mesh(defaultMaterial, new Vector3(100, 0, 0), Vector3.Zero);
plane.ImportFromFile("Plane.obj");
playground.AddMesh(plane);
gameWindow.Run();
}
}
}
Vertex Shader
#version 330 core
layout (location = 0) in vec3 aPosition;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = vec4(aPosition, 1.0) * model * view * projection;
}
Fragment Shader
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
New contributor