I've been trying to build 3D rendering engine for some time. My main source of knowledge about OpenGL is amazing www.learnopengl.com so is my code design heavily inspired by Joe's examples. What I didn't like about his approach - I know educational code is different beast from enterprise solutions - is no connections between shader logic and its implementation.

In the rendering process there are used many shaders, each can differ from each other a lot. Phong shader and particle shader in its logic might have nothing in common. So in my design base class for all Shaders only takes care of OpenGL stuff and serves as utility class (e.x. reading source file for different stages) ShaderProgram.h

#ifndef ShaderProgram_h
#define ShaderProgram_h

#include "../debuging/Logger.h"
#include "../core/Enum.h"

#pragma warning(push, 0)
#include <glad/glad.h>
#include <glm/glm.hpp>
#pragma warning(pop)

#include <assert.h>
#include <vector>
#include <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include <optional>

constexpr GLsizei infolog_max_length = 1024;

namespace zephyr::rendering {

class ICamera;

class ShaderProgram {
    ShaderProgram(const std::string& name, const std::string& vertex_code, const std::string& fragment_code, const std::string& geometry_code);

    ShaderProgram(const ShaderProgram&) = delete;
    ShaderProgram& operator=(const ShaderProgram&) = delete;
    ShaderProgram(ShaderProgram&&) = delete;
    ShaderProgram& operator=(ShaderProgram&&) = delete;
    virtual ~ShaderProgram();

    void Use() const;

    GLuint ID() const { return m_ID.value(); }
    const std::string& Name() const { return m_Name; }

    virtual void Draw(const ICamera* camera) = 0;

    void Uniform(const std::string &name, bool value) const;
    void Uniform(const std::string &name, int value) const;
    void Uniform(const std::string &name, float value) const;
    void Uniform(const std::string &name, const glm::vec2 &vec) const;
    void Uniform(const std::string &name, float x, float y) const;
    void Uniform(const std::string &name, const glm::vec3 &vec) const;
    void Uniform(const std::string &name, float x, float y, float z) const;
    void Uniform(const std::string &name, const glm::vec4 &vec) const;
    void Uniform(const std::string &name, float x, float y, float z, float w) const;
    void Uniform(const std::string &name, const glm::mat2 &mat) const;
    void Uniform(const std::string &name, const glm::mat3 &mat) const;
    void Uniform(const std::string &name, const glm::mat4 &mat) const;

    std::string ReadShaderFile(const std::string& path);

    std::optional<GLuint> m_ID;
    std::string m_Name;

    void LinkProgram();
    std::optional<GLuint> CompileShader(const std::string& code, GLenum shader);




#include "ShaderProgram.h"

zephyr::rendering::ShaderProgram::ShaderProgram(const std::string& name, const std::string& vertex_path, const std::string& fragment_path, const std::string& geometry_path)
    : m_Name(name) {
    m_ID = glCreateProgram();

    auto vertex_shader = CompileShader(vertex_path, GL_VERTEX_SHADER);
    auto fragment_shader = CompileShader(fragment_path, GL_FRAGMENT_SHADER);
    auto geometry_shader = !geometry_path.empty() ? CompileShader(geometry_path, GL_GEOMETRY_SHADER) : std::nullopt;

    assert(vertex_shader.has_value() && fragment_shader.has_value());



zephyr::rendering::ShaderProgram::~ShaderProgram() {

void zephyr::rendering::ShaderProgram::Use() const {

void zephyr::rendering::ShaderProgram::Uniform(const std::string &name, bool value) const {
    glUniform1i(glGetUniformLocation(m_ID.value(), name.c_str()), (int)value);

void zephyr::rendering::ShaderProgram::Uniform(const std::string &name, int value) const {
    glUniform1i(glGetUniformLocation(m_ID.value(), name.c_str()), value);

void zephyr::rendering::ShaderProgram::Uniform(const std::string &name, float value) const {
    glUniform1f(glGetUniformLocation(m_ID.value(), name.c_str()), value);

void zephyr::rendering::ShaderProgram::Uniform(const std::string &name, const glm::vec2 &vec) const {
    glUniform2fv(glGetUniformLocation(m_ID.value(), name.c_str()), 1, &vec[0]);

void zephyr::rendering::ShaderProgram::Uniform(const std::string &name, float x, float y) const {
    glUniform2f(glGetUniformLocation(m_ID.value(), name.c_str()), x, y);

void zephyr::rendering::ShaderProgram::Uniform(const std::string &name, const glm::vec3 &vec) const {
    glUniform3fv(glGetUniformLocation(m_ID.value(), name.c_str()), 1, &vec[0]);

void zephyr::rendering::ShaderProgram::Uniform(const std::string &name, float x, float y, float z) const {
    glUniform3f(glGetUniformLocation(m_ID.value(), name.c_str()), x, y, z);

void zephyr::rendering::ShaderProgram::Uniform(const std::string &name, const glm::vec4 &vec) const {
    glUniform4fv(glGetUniformLocation(m_ID.value(), name.c_str()), 1, &vec[0]);

void zephyr::rendering::ShaderProgram::Uniform(const std::string &name, float x, float y, float z, float w) const {
    glUniform4f(glGetUniformLocation(m_ID.value(), name.c_str()), x, y, z, w);

void zephyr::rendering::ShaderProgram::Uniform(const std::string &name, const glm::mat2 &mat) const {
    glUniformMatrix2fv(glGetUniformLocation(m_ID.value(), name.c_str()), 1, GL_FALSE, &mat[0][0]);

void zephyr::rendering::ShaderProgram::Uniform(const std::string &name, const glm::mat3 &mat) const {
    glUniformMatrix3fv(glGetUniformLocation(m_ID.value(), name.c_str()), 1, GL_FALSE, &mat[0][0]);

void zephyr::rendering::ShaderProgram::Uniform(const std::string &name, const glm::mat4 &mat) const {
    glUniformMatrix4fv(glGetUniformLocation(m_ID.value(), name.c_str()), 1, GL_FALSE, &mat[0][0]);

std::string zephyr::rendering::ShaderProgram::ReadShaderFile(const std::string& path) {
    std::fstream shader_file;
    std::stringstream shader_stream;

    shader_file.exceptions(std::ifstream::failbit | std::ifstream::badbit);

    // Read file
    try {
        shader_stream << shader_file.rdbuf();
    catch (const std::ifstream::failure & e) {
        ERROR_LOG(Logger::ESender::Rendering, "Failed to read shader file %s:\n%s", path, e.what());
        return "";

    return shader_stream.str();

std::optional<GLuint> zephyr::rendering::ShaderProgram::CompileShader(const std::string& code, GLenum shader_type) {
    // Compile shader
    GLuint shader = glCreateShader(shader_type);
    const GLchar* shader_code_ptr = code.c_str();
    glShaderSource(shader, 1, &shader_code_ptr, nullptr);
    // Check compile errors
    GLint success;
    GLchar info_log[infolog_max_length];
    glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(shader, infolog_max_length, nullptr, info_log);
        ERROR_LOG(Logger::ESender::Rendering, "Failed to compile shader %d:\n%s", shader_type, info_log);
        return std::optional<GLuint>();
    glAttachShader(m_ID.value(), shader);
    return shader;

void zephyr::rendering::ShaderProgram::LinkProgram() {
    // Check linking errors
    GLint success;
    GLchar info_log[infolog_max_length];
    glGetProgramiv(m_ID.value(), GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(m_ID.value(), infolog_max_length, nullptr, info_log);
        ERROR_LOG(Logger::ESender::Rendering, "Failed to link shader %d:\n%s", m_ID.value(), info_log);

I've mentioned Phong many times earlier because I think it's a good example of shader with advanced logic. Since this class represent rendering logic behind Phong's it's make sense for it to also define structures that it uses. The most important it's own implementation of Model/Mesh. I really don't like idea of general use Model class that can take any shader and any model data (vertices, normals end others). Phong.h

#ifndef Phong_h
#define Phong_h

#include "../ShaderProgram.h"
#include "../IDrawable.h"

#pragma warning(push, 0)
#include <glm/glm.hpp>
#pragma warning(pop)

#include <vector>

namespace zephyr::resources {
    class Model;
    class Mesh;

namespace zephyr::rendering {

class Texture;
class IRenderListener;

class Phong : public ShaderProgram {

    struct DirectionalLight {
        glm::vec3 Direction{ 0.0f };
        glm::vec3 Ambient{ 0.0f };
        glm::vec3 Diffuse{ 0.0f };
        glm::vec3 Specular{ 0.0f };

    class StaticModel;

    Phong(const Phong&) = delete;
    Phong& operator=(const Phong&) = delete;
    Phong(Phong&&) = delete;
    Phong& operator=(Phong&&) = delete;
    ~Phong() = default;

    void Draw(const ICamera* camera) override;

    void SetDirectionalLight(const glm::vec3& direction, const glm::vec3& ambient, const glm::vec3& diffuse, const glm::vec3& specular);

    void Register(StaticModel* static_model);
    void Unregister(StaticModel* static_mocel);

    std::vector<StaticModel*> m_Drawables;

    DirectionalLight m_DirectionalLight;

class Phong::StaticModel : public IDrawable {
    class StaticMesh {
        explicit StaticMesh(const resources::Mesh& raw_StaticMesh);

        StaticMesh() = delete;
        StaticMesh(const StaticMesh&) = delete;
        StaticMesh& operator=(const StaticMesh&) = delete;
        StaticMesh(StaticMesh&& other) noexcept;
        StaticMesh& operator=(StaticMesh&& other) noexcept;

        void Draw(const ShaderProgram& shader) const;

        GLuint VAO() const { return m_VAO; }
        GLuint VBO() const { return m_VBO; }
        GLuint EBO() const { return m_EBO; }
        GLsizei IndicesCount() const { return m_IndicesCount; }
        const Texture* Diffuse() const { return m_Diffuse.get(); }
        const Texture* Specular() const { return m_Specular.get(); }
        float Shininess() const { return m_Shininess; }

        GLuint m_VAO;
        GLuint m_VBO;
        GLuint m_EBO;
        GLsizei m_IndicesCount;
        std::unique_ptr<Texture> m_Diffuse{ nullptr };
        std::unique_ptr<Texture> m_Specular{ nullptr };
        float m_Shininess;

    explicit StaticModel(const resources::Model& raw_model);

    StaticModel() = delete;
    StaticModel(const StaticModel&) = delete;
    StaticModel& operator=(const StaticModel&) = delete;
    StaticModel(StaticModel&&) = default;
    StaticModel& operator=(StaticModel&&) = default;
    ~StaticModel() = default;

    void Draw(const ShaderProgram& shader) const override;

    void ModelMatrix(const glm::mat4& matrix_model);
    glm::mat4 ModelMatrix() const;

    std::vector<StaticModel::StaticMesh> m_StaticMeshes;
    glm::mat4 m_Model{0.0f};




#include "Phong.h"
#include "../ICamera.h"
#include "../IRenderListener.h"
#include "../Texture.h"
#include "../../resources/Model.h"
#include "../../resources/Mesh.h"

zephyr::rendering::Phong::StaticModel::StaticModel(const resources::Model& raw_model) {
    for (auto it = raw_model.RawMeshes().begin(); it != raw_model.RawMeshes().end(); it++) {

void zephyr::rendering::Phong::StaticModel::Draw(const ShaderProgram& shader) const {
    shader.Uniform("model", m_Model);
    for (auto it = m_StaticMeshes.begin(); it != m_StaticMeshes.end(); it++) {

void zephyr::rendering::Phong::StaticModel::ModelMatrix(const glm::mat4& matrix_model) {
    m_Model = matrix_model;

glm::mat4 zephyr::rendering::Phong::StaticModel::ModelMatrix() const {
    return m_Model;

zephyr::rendering::Phong::StaticModel::StaticMesh::StaticMesh(const resources::Mesh& raw_mesh)
    : m_IndicesCount(raw_mesh.Indices().size())
    , m_Shininess(static_cast<float>(raw_mesh.Shininess())) {
    if (raw_mesh.Diffuse()) {
        m_Diffuse = std::make_unique<Texture>(*raw_mesh.Diffuse(), Texture::EType::Diffuse);

    if (raw_mesh.Specular()) {
        m_Specular = std::make_unique<Texture>(*raw_mesh.Specular(), Texture::EType::Specular);

    glGenVertexArrays(1, &m_VAO);
    glGenBuffers(1, &m_VBO);
    glGenBuffers(1, &m_EBO);


    glBindBuffer(GL_ARRAY_BUFFER, m_VBO);
    glBufferData(GL_ARRAY_BUFFER, raw_mesh.Vertices().size() * sizeof(resources::Mesh::Vertex), &raw_mesh.Vertices()[0], GL_STATIC_DRAW);

    // Position
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(resources::Mesh::Vertex), (void*)0);

    // Normal vectors
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(resources::Mesh::Vertex), (void*)offsetof(resources::Mesh::Vertex, resources::Mesh::Vertex::Normal));

    // Image coords
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(resources::Mesh::Vertex), (void*)offsetof(resources::Mesh::Vertex, resources::Mesh::Vertex::TexCoords));

    glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_IndicesCount * sizeof(unsigned int), &raw_mesh.Indices()[0], GL_STATIC_DRAW);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

zephyr::rendering::Phong::StaticModel::StaticMesh::StaticMesh(StaticMesh&& other) noexcept
    : m_VAO(std::exchange(other.m_VAO, 0))
    , m_VBO(std::exchange(other.m_VBO, 0))
    , m_EBO(std::exchange(other.m_EBO, 0))
    , m_Diffuse(std::move(other.m_Diffuse))
    , m_Specular(std::move(other.m_Specular)) {
    m_IndicesCount = other.m_IndicesCount;
    m_Shininess = other.m_Shininess;

zephyr::rendering::Phong::StaticModel::StaticMesh& zephyr::rendering::Phong::StaticModel::StaticMesh::operator=(StaticMesh&& other) noexcept {
    m_VAO = std::exchange(other.m_VAO, 0);
    m_VBO = std::exchange(other.m_VBO, 0);
    m_EBO = std::exchange(other.m_EBO, 0);
    m_Diffuse = std::move(other.m_Diffuse);
    m_Specular = std::move(other.m_Diffuse);
    m_IndicesCount = other.m_IndicesCount;
    m_Shininess = other.m_Shininess;

    return *this;

zephyr::rendering::Phong::StaticModel::StaticMesh::~StaticMesh() {
    glDeleteVertexArrays(1, &m_VAO);
    glDeleteBuffers(1, &m_VBO);
    glDeleteBuffers(1, &m_EBO);

void zephyr::rendering::Phong::StaticModel::StaticMesh::Draw(const ShaderProgram& shader) const {
    if (m_Diffuse) {
        shader.Uniform("material.diffuse", 0);
        glBindTexture(GL_TEXTURE_2D, m_Diffuse->ID());

    if (m_Specular) {
        shader.Uniform("material.specular", 1);
        glBindTexture(GL_TEXTURE_2D, m_Specular->ID());

    shader.Uniform("material.shininess", m_Shininess);

    glDrawElements(GL_TRIANGLES, m_IndicesCount, GL_UNSIGNED_INT, 0);


    : ShaderProgram(
    "") { }

void zephyr::rendering::Phong::Draw(const ICamera* camera) {
    glm::mat4 pv = camera->Projection() * camera->View();

    Uniform("directionalLight.direction", m_DirectionalLight.Direction);
    Uniform("directionalLight.ambient", m_DirectionalLight.Ambient);
    Uniform("directionalLight.diffuse", m_DirectionalLight.Diffuse);
    Uniform("directionalLight.specular", m_DirectionalLight.Specular);

    Uniform("pv", pv);
    Uniform("viewPosition", camera->LocalPosition());

    for (auto& drawable : m_Drawables) {
        auto user_pointer = static_cast<IRenderListener*>(drawable->UserPointer());

void zephyr::rendering::Phong::SetDirectionalLight(const glm::vec3& direction, const glm::vec3& ambient, const glm::vec3& diffuse, const glm::vec3& specular) {
    m_DirectionalLight.Direction = direction;
    m_DirectionalLight.Ambient = ambient;
    m_DirectionalLight.Diffuse = diffuse;
    m_DirectionalLight.Specular = specular;

void zephyr::rendering::Phong::Register(StaticModel* static_model) {
    assert(std::find(m_Drawables.begin(), m_Drawables.end(), static_model) == m_Drawables.end());

void zephyr::rendering::Phong::Unregister(StaticModel* static_mocel) {
    auto to_erase = std::find(m_Drawables.begin(), m_Drawables.end(), static_mocel);
    if (to_erase != m_Drawables.end()) {

I've cut some other code from it like PointLight struct as it's almost identical to DirectionalLight which should present design on it's own. And also didn't show some included class but theirs implementation is not important. So my question would be what do you think about architecture as this? I would love to hear your opinion, maybe pros and cons about this approach and possible pitfalls I don't see.


Some suggestions re. the ShaderProgram class:

  • The code reading input from a file shouldn't be inside the ShaderProgram class. We should do that separately and pass in the actual source string.

  • OpenGL shaders programs don't always use one shader source file (or indeed one shader object) for each stage. We often want to use multiple fragment shader objects in a single program.

    (e.g. We might want a file with a bunch of utility constants, e.g. pi. Or for deferred lighting, we want a single fragment shader object to fetch data from the gbuffers, and use that one object in several different lighting-related shader programs.)

    • ... so we might want a constructor taking a std::vector of source strings (and shader types) or shader objects.

    • ... and it might be better to write a separate ShaderObject class.

  • OpenGL uses 0 for an invalid shader object / program ID. So we don't really need to use std::optional for the ID.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.