Organizing Project (OpenGL OOP)

The project needed a little bit of organization. I decided to add some manager classes and I tried to wrap OpenGL operations. In this way, the project can be maintainable and readable. I tried to create an architecture with the best fit in my mind, but then it will probably need more improvement. I don't claim that this organization is a very good implementation certainly.

The project directory will look like it at the end of the post:
  • managers/
    • ObjectManager.cs
    • RenderManager.cs
    • SceneManager.cs
    • ShaderManager.cs
    • VAOManager.cs
  • objects/
    • components/
      • Transform.cs
    • GameObject.cs
    • Mesh.cs
    • Scene.cs
  • shaders/
    • triangle/
      • vertexshader.glsl
      • fragmentshader.glsl
    • Shader.cs
  • App.cs
  • Program.cs
  • Window.cs
I'm going to explain briefly which file will be responsible for what. 

SceneManager.cs: This manager responsible for scene objecs. These scene objects will be updated and rendered in the SceneManager class.
using OpenTK.Graphics.OpenGL;
using SFML.Window;

namespace OpenTKTutorial
{
    public class SceneManager
    {
        public Dictionary<string, Scene> Scenes;
        public string ActiveScene {get; set;}

        public SceneManager()
        {   
            Scenes = new Dictionary<string, Scene>();
        }
        public void AddScene(String sceneName, Scene scene)
        {
            Scenes[sceneName] = scene;
            ActiveScene = sceneName;
        }

        public void Update()
        {
            Scenes[ActiveScene].Update();
        }

        public void Draw()
        {
            Scenes[ActiveScene].Draw();
        }
    }
}
Scene.cs: This object is representation of the scene obviously. Scene object will be contain four references called ObjectManager, RenderManager,  SceneManager, ShaderManager. We added our game objects from here for now.
using OpenTK.Graphics.OpenGL;
using SFML.Window;

namespace OpenTKTutorial
{
    public class Scene
    {
        RenderManager renderManager;
        VAOManager vaoManager;
        ObjectManager objectManager;
        ShaderManager shaderManager;

        public Scene()
        {
            vaoManager = new VAOManager();
            shaderManager = new ShaderManager();
            objectManager = new ObjectManager(vaoManager, shaderManager);
            renderManager = new RenderManager(objectManager);

            InitScene();
        }

        private void InitScene()
        {
            for (int i = 0; i < 1000; i++)
            {
                objectManager.AddObject(ObjectType.Quad2D);
                objectManager.gameObjects[i].Transform.Position = new OpenTK.Mathematics.Vector3(new Random().Next(-1000,1000) / 1000f, new Random().Next(-1000,1000) / 1000f, 0);
            }
            // objectManager.AddObject(ObjectType.Quad2D);
        }

        public void Update()
        {
            objectManager.Update();
        }

        public void Draw()
        {
            renderManager.Draw();
        }
    }
}
VAOManager.cs: This manager responsible for Vertex Array Object IDs and their index count. We will create VAO from this class. I used Dictionary structure to manage easily. If VAO is already created for one of the ObjectTypes, we don't need to create another VAO for the same ObjectType. ObjectType defined as enum in the GameObject class.
using OpenTK.Graphics.OpenGL;
using SFML.Window;

namespace OpenTKTutorial
{
    public class VAOManager
    {
        Dictionary<ObjectType, int> vaos;
        Dictionary<ObjectType, int> indexCounts;

        public Dictionary<ObjectType, int> VAOs { get { return vaos; } }
        public Dictionary<ObjectType, int> IndexCounts { get { return indexCounts; } }
        
        public VAOManager()
        {
            vaos = new Dictionary<ObjectType, int>();
            indexCounts = new Dictionary<ObjectType, int>();
        }

        public void CreateVAO(ObjectType objType)
        {
            if(!VAOs.ContainsKey(objType))
            {
                Mesh mesh = new Mesh(objType);
                vaos[objType] = mesh.VAO;
                indexCounts[objType] = mesh.IndexCount;
            }
        }
    }    
}
Mesh.cs: This classes represent of the shape as primitive. It will contains VBO, VAO and EBO ids. We will call this class to create VAO in the VAOManager class. We will create specific shapes like cube, circle, quad, etc. through this class.
using OpenTK.Graphics.OpenGL;
using SFML.Window;

namespace OpenTKTutorial
{
    public class Mesh
    {
        int vao, vbo, ebo;
        int indexCount;

        public int VAO { get { return vao; }  }
        public int VBO { get { return vbo; } }
        public int EBO { get { return ebo; } }
        public int IndexCount {get {return indexCount; } }

        public Mesh(ObjectType objType)
        {
            if(objType is ObjectType.Quad2D) GenerateQuad();
        }

        private void GenerateQuad()
        {   
            float len = 0.01f;

            float[] vertices = 
            {
                -len,  len, 0f,
                 len,  len, 0f,
                 len, -len, 0f,
                -len, -len, 0f
            };

            int[] indices = 
            {
                0, 1, 3,
                1, 2, 3
            };

            indexCount = 6;

            GenerateVAO(len, vertices, indices);
        }

        private void GenerateVAO(float len, float[] vertices, int[] indices)
        {
            GL.GenVertexArrays(1, out vao);
            GL.BindVertexArray(vao);
            // create vbo to store the data in opengl and copy data to vbo
            GL.GenBuffers(1, out vbo);
            GL.BindBuffer(BufferTarget.ArrayBuffer, vbo);
            GL.BufferData(BufferTarget.ArrayBuffer, sizeof(float) * vertices.Length, vertices, BufferUsageHint.StaticDraw);
            // create ebo
            GL.GenBuffers(1, out ebo);
            GL.BindBuffer(BufferTarget.ElementArrayBuffer, ebo);
            GL.BufferData(BufferTarget.ElementArrayBuffer, sizeof(int) * indices.Length, indices, BufferUsageHint.StaticDraw);
            // tell opengl how to use the data via attributes
            // position attribute
            // int attrPosition_Position = GL.GetAttribLocation(shader.ShaderProgram, "aPos");
            int attrPosition_Position = 0;
            GL.EnableVertexAttribArray(attrPosition_Position);
            GL.VertexAttribPointer(attrPosition_Position, 3, VertexAttribPointerType.Float, false, 0, 0);
            
            GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
            GL.BindVertexArray(0);
        }
    }
}
ShaderManager.cs: This class responsible for shaders. I'm going to store shaders of ObjectType like Quad2d, Cube via this class. Also, we will create shaders from this class like what we did in VAOManager:
using OpenTK.Graphics.OpenGL;
using SFML.Window;

namespace OpenTKTutorial
{
    public class ShaderManager
    {
        Dictionary<ObjectType, Shader> shaders;

        public Dictionary<ObjectType, Shader> Shaders { get { return shaders; } }

        public ShaderManager()
        {   
            shaders = new Dictionary<ObjectType, Shader>();
        }

        public void CreateShader(ObjectType objectType)
        {
            if(!Shaders.ContainsKey(objectType))
            {
                if(objectType == ObjectType.Quad2D) 
                {
                    Shader shader = new Shader("shaders/triangle/vertexShader.glsl","shaders/triangle/fragmentShader.glsl");
                    shaders[objectType] = shader;
                }       
            }
        }
    }
}
ObjectManager.cs: This class responsible for GameObjects. It have references of VAOManager and ShaderManager objects. We will add new GameObject via this class and also we are going to update these existed GameObjects in the update method:
using OpenTK.Graphics.OpenGL;
using SFML.Window;

namespace OpenTKTutorial
{
    public class ObjectManager
    {   
        private VAOManager VAOManager;
        private ShaderManager ShaderManager;
        public List<GameObject> gameObjects;

        public ObjectManager(VAOManager VAOManager, ShaderManager ShaderManager)
        {
            gameObjects = new List<GameObject>();
            this.VAOManager = VAOManager;
            this.ShaderManager = ShaderManager;
        }

        public void AddObject(ObjectType objectType)
        {
            this.VAOManager.CreateVAO(objectType);
            this.ShaderManager.CreateShader(objectType);
            GameObject gameObject = new GameObject(objectType);
            gameObject.VAO = this.VAOManager.VAOs[objectType];
            gameObject.IndexCount = this.VAOManager.IndexCounts[objectType];
            gameObject.Shader = this.ShaderManager.Shaders[objectType];
            this.gameObjects.Add(gameObject);
        }

        public void Update()
        {
            foreach (var obj in gameObjects)
            {   
                obj.Update();
            }
        }
    }
}
RenderManager.cs: This class responsible for just rendering. Probably, I'm not using it properly. But for now, keep it that way, I'll most likely update it in the next posts.
using OpenTK.Graphics.OpenGL;
using SFML.Window;

namespace OpenTKTutorial
{
    public class RenderManager
    {
        private ObjectManager? objectManager;

        public RenderManager(ObjectManager objectManager)
        {
            this.objectManager = objectManager;
        }

        private void Clear()
        {
            GL.Clear(ClearBufferMask.ColorBufferBit);
        }

        public void Draw()
        {
            Clear();

            for (int i = 0; i < objectManager!.gameObjects.Count; i++)
            {
                objectManager.gameObjects[i].Draw();
            }
        }
    }
}
GameObject.cs: It is a GameoObject class. I also defined ObjectType as enum in this file. GameObject contains VAO id, IndexCount, Shader reference, and Transform as a component. I also added a draw method to draw it. We call this method from RenderManager class:
using OpenTK.Graphics.OpenGL;
using SFML.Window;

namespace OpenTKTutorial
{
    public enum ObjectType
    {
        Cube,
        Plane,
        Triangle2D,
        Quad2D
    }

    public class GameObject
    {
        ObjectType objectType;

        public int VAO { get; set; }
        public int IndexCount { get; set; }
        public Shader? Shader {get; set;}
        public Transform Transform {get; set;}

        public GameObject(ObjectType objectType)
        {
            Transform = new Transform();
            this.objectType = objectType;
        }

        public void Update()
        {
            
        }

        public void Draw()
        {
            GL.UseProgram(Shader!.ShaderProgram);

            GL.Uniform3(0, Transform.Position);

            GL.BindVertexArray(VAO);
            GL.DrawElements(PrimitiveType.Triangles, IndexCount, DrawElementsType.UnsignedInt, 0);
            GL.BindVertexArray(0);
        }
    }
}
Transform.cs: Transform is a component for game objects. It will store Position, Rotation, and Scale values of a game object:
using OpenTK.Graphics.OpenGL;
using SFML.Window;

namespace OpenTKTutorial
{
    public class Transform
    {
        public OpenTK.Mathematics.Vector3 Position {get; set;}
        public OpenTK.Mathematics.Vector3 Rotation {get; set;}
        public OpenTK.Mathematics.Vector3 Scale {get; set;}

        public Transform()
        {
            Position = new OpenTK.Mathematics.Vector3(0,0,0);
            Rotation = new OpenTK.Mathematics.Vector3(0,0,0);
            Scale = new OpenTK.Mathematics.Vector3(0,0,0);
        }
    }
}
After that, we just need to use SceneManager object in App class. We've freed the App class pretty much from complexity. If 1000 game objects are drawn successfully on the screen as in the image, there is no problem:
test of the organized project


No comments:

Post a Comment