Advanced Shader Programming: My Journey into Real-Time 3D Rendering!

Dive into a C++ & OpenGL Graphic Engine

Project Overview & Architecture: Unpacking My Engine!

So, I built this real-time 3D rendering engine in C++ and OpenGL, and let me tell you, it was a blast diving deep into advanced shader programming! My main goal was to craft a super modular and efficient rendering pipeline that could handle awesome dynamic terrain, realistic lighting, and cool animated elements like billboards. Plus, I baked in an ImGui interface for real-time tweaking – because who wants to recompile every time you want to see a change, right? This engine really shows off my passion for low-level graphics and squeezing out every drop of performance!

  • My Engine's Blueprint (The Architecture Diagram)

Visuals make everything clearer! Here's a conceptual diagram of how all the pieces fit together.

+-------------------------------------------------------------+
|                     Application / TestScene                 |
|            (The brain coordinating everything!)             |
+-------------------------------------------------------------+
       |
       v
+-------------------------------------------------------------+
|                       Renderer & RenderState                |
|           (My conductor for drawing, managing OpenGL state) |
+-------------------------------------------------------------+
       |          ^          ^           ^           ^
       |          |          |           |           |
       v          |          |           |           |
+-------------+ +------------+ +----------+ +-----------+ +-------------+
|   Shader    | |   Model    | |  Terrain   | |  SkyBox    | |  Billboard  |
| (My GLSL    | | (Assimp    | | (Heightmap)| | (Cubemap)  | | (Geometry   |
| magic!)     | | Loader)    | | (Tess'n!)  | |            | |  Shader!)   |
+-------------+ +------------+ +----------+ +-----------+ +-------------+


Note: This is a simplified version of the project it does not 
include all the engine components

Core Technical Systems: Where the Real Fun Happens!

This is where I get to brag about the cool tech I implemented! Each section gets straight to the point, with a code snippet and a suggested screenshot.

  1. Dynamic Terrain: Tessellation & Heightmaps! ⛰️
    I had a blast building a super optimized procedural terrain generation system! It lets me create huge, detailed "The details can be set by the user because it has a huge impact on the FPS, the user can do this by using the UI that i made using imGui" landscapes that look great and run smoothly. My secret? Tessellation shaders that dynamically crank up the detail when you're close and scale it back when you're far away. Plus, heightmaps let me sculpt mountains and valleys without ever touching a modeling tool!

  1. Dynamic Terrain: Tessellation & Heightmaps! ⛰️
    I had a blast building a super optimized procedural terrain generation system! It lets me create huge, detailed "The details can be set by the user because it has a huge impact on the FPS, the user can do this by using the UI that i made using imGui" landscapes that look great and run smoothly. My secret? Tessellation shaders that dynamically crank up the detail when you're close and scale it back when you're far away. Plus, heightmaps let me sculpt mountains and valleys without ever touching a modeling tool!

  1. Dynamic Terrain: Tessellation & Heightmaps! ⛰️
    I had a blast building a super optimized procedural terrain generation system! It lets me create huge, detailed "The details can be set by the user because it has a huge impact on the FPS, the user can do this by using the UI that i made using imGui" landscapes that look great and run smoothly. My secret? Tessellation shaders that dynamically crank up the detail when you're close and scale it back when you're far away. Plus, heightmaps let me sculpt mountains and valleys without ever touching a modeling tool!

  1. Lighting it Up: My Phong Model!
    I poured a lot of effort into getting the lighting just right. My real-time dynamic lighting system uses the classic Phong lighting model to bring scenes to life! It's all about calculating ambient, diffuse, and specular components for everything—terrain, models, you name it—to get that realistic glow. Getting those normal vectors transformed perfectly in the shaders was key to making it all look natural.

  1. Lighting it Up: My Phong Model!
    I poured a lot of effort into getting the lighting just right. My real-time dynamic lighting system uses the classic Phong lighting model to bring scenes to life! It's all about calculating ambient, diffuse, and specular components for everything—terrain, models, you name it—to get that realistic glow. Getting those normal vectors transformed perfectly in the shaders was key to making it all look natural.

  1. Lighting it Up: My Phong Model!
    I poured a lot of effort into getting the lighting just right. My real-time dynamic lighting system uses the classic Phong lighting model to bring scenes to life! It's all about calculating ambient, diffuse, and specular components for everything—terrain, models, you name it—to get that realistic glow. Getting those normal vectors transformed perfectly in the shaders was key to making it all look natural.

  1. My Modular Rendering Pipeline: Clean & Lean! 🛠️
    Building this engine, I really focused on a clean, modular rendering pipeline. My Renderer and RenderState classes work hand-in-hand to keep things super efficient by cutting down on redundant OpenGL calls. This setup makes it a breeze to swap out shaders, add new features, or just keep the whole system tidy and performant!

  1. My Modular Rendering Pipeline: Clean & Lean! 🛠️
    Building this engine, I really focused on a clean, modular rendering pipeline. My Renderer and RenderState classes work hand-in-hand to keep things super efficient by cutting down on redundant OpenGL calls. This setup makes it a breeze to swap out shaders, add new features, or just keep the whole system tidy and performant!

  1. My Modular Rendering Pipeline: Clean & Lean! 🛠️
    Building this engine, I really focused on a clean, modular rendering pipeline. My Renderer and RenderState classes work hand-in-hand to keep things super efficient by cutting down on redundant OpenGL calls. This setup makes it a breeze to swap out shaders, add new features, or just keep the whole system tidy and performant!

  1. Billboards: Objects That Always Face You! 🌳
    I got to implement a really neat trick called billboarding using a geometry shader! This makes objects like trees and other small elements always face the camera, no matter where you move. It's super efficient because the GPU literally turns a single point into a camera-facing quad, keeping the scene looking consistent and detailed.

  1. Challenges & My Solutions: Learning and Growing! 💪
    This section is all about showing off how I tackle problems head-on.

    • Challenge: Balancing Terrain Detail & Performance! 📈

      • Problem: Getting beautiful, highly detailed terrain while keeping a silky-smooth frame rate was super tricky. And honestly, integrating heightmaps to sculpt those hills felt like a puzzle at first!

      • My Solution: I figured it out by fine-tuning the distance-dependent tessellation (big win!) and really optimizing the heightmap application in my Terrain.cpp. Plus, calculating normals with the Central Difference Method (CDM) made the lighting super realistic without bogging down the GPU.

        Challenge: Taming the Shader Pipeline! 🔗

        • Problem: Juggling multiple shader stages (tessellation, geometry, fragment) and keeping all those OpenGL states synchronized felt like herding cats! It was complex and could easily kill performance.

        • My Solution: This is where my modular Renderer and RenderState class architecture truly shined. The RenderState became my secret weapon, efficiently tracking and minimizing redundant OpenGL state changes, which massively streamlined the whole pipeline and boosted performance. My Shader.cpp also got a robust multi-shader loading mechanism, making integration a breeze.

      • Challenge: Billboards Looking... Less Than Ideal! 🖼️

        • Problem: My initial billboards had this annoying white background and just wouldn't face the camera right, making them look totally out of place.

        • My Solution: After some serious tweaking in my BillboardGeo.glsl geometry shader , I nailed the camera-facing orientation. I also refined my Texture.cpp for better texture loading, ensuring transparency worked perfectly.

My Takeaway & What's Next! 🚀

This project was an absolute game-changer for me. I gained a ton of hands-on experience in real-time rendering, mastering advanced shader techniques, and really digging into performance optimization for graphics. From crafting dynamic terrain to making lighting feel realistic and implementing super-efficient billboarding, every single system I built taught me so much about iterative development and technical problem-solving. This project has seriously leveled up my foundation in game engine development and computer graphics, and I'm beyond excited to tackle even wilder challenges in the future!

This project was an absolute game-changer for me. I gained a ton of hands-on experience in real-time rendering, mastering advanced shader techniques, and really digging into performance optimization for graphics. From crafting dynamic terrain to making lighting feel realistic and implementing super-efficient billboarding, every single system I built taught me so much about iterative development and technical problem-solving. This project has seriously leveled up my foundation in game engine development and computer graphics, and I'm beyond excited to tackle even wilder challenges in the future!

This project was an absolute game-changer for me. I gained a ton of hands-on experience in real-time rendering, mastering advanced shader techniques, and really digging into performance optimization for graphics. From crafting dynamic terrain to making lighting feel realistic and implementing super-efficient billboarding, every single system I built taught me so much about iterative development and technical problem-solving. This project has seriously leveled up my foundation in game engine development and computer graphics, and I'm beyond excited to tackle even wilder challenges in the future!

Code to Show Off!

1. Shader Class Constructor: Building My Shader Programs!

This snippet from my Shader.cpp shows how I dynamically load, compile, and link various types of shaders—including vertex, fragment, geometry, tessellation control, and tessellation evaluation shaders—into a single OpenGL program. It’s all about creating a flexible system that can handle even the most complex shader pipelines.

1. Shader Class Constructor: Building My Shader Programs!

This snippet from my Shader.cpp shows how I dynamically load, compile, and link various types of shaders—including vertex, fragment, geometry, tessellation control, and tessellation evaluation shaders—into a single OpenGL program. It’s all about creating a flexible system that can handle even the most complex shader pipelines.

1. Shader Class Constructor: Building My Shader Programs!

This snippet from my Shader.cpp shows how I dynamically load, compile, and link various types of shaders—including vertex, fragment, geometry, tessellation control, and tessellation evaluation shaders—into a single OpenGL program. It’s all about creating a flexible system that can handle even the most complex shader pipelines.

#include <shader.h> // Assuming this includes necessary OpenGL headers and defines Shader class
#include <fstream>
#include <sstream>
#include <iostream>

// Constructor for creating a shader program from various shader file paths
Shader::Shader(const char* vertexPath, const char* fragmentPath, const char* geometryPath,
	const char* TCSPath, const char* TESPath)
{
	// 1. Retrieve the shader source code from file paths
	std::string vertexCode;
	std::string fragmentCode;
	std::string geometryCode;
	std::string TCSCode;
	std::string TESCode;

	std::ifstream vShaderFile;
	std::ifstream fShaderFile;
	std::ifstream gShaderFile;
	std::ifstream tcsShaderFile;
	std::ifstream tesShaderFile;

	// Ensure ifstream objects can throw exceptions for robust file handling
	vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
	fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
	gShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
	tcsShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
	tesShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);

	try
	{
		// Open and read vertex and fragment shader files
		vShaderFile.open(vertexPath);
		fShaderFile.open(fragmentPath);
		std::stringstream vShaderStream, fShaderStream;
		vShaderStream << vShaderFile.rdbuf(); // Read file's buffer contents into streams
		fShaderStream << fShaderFile.rdbuf();
		vShaderFile.close();
		fShaderFile.close();
		vertexCode = vShaderStream.str(); // Convert streams to strings
		fragmentCode = fShaderStream.str();

		// Conditionally open and read geometry shader file
		if (geometryPath != nullptr)
		{
			gShaderFile.open(geometryPath);
			std::stringstream gShaderStream;
			gShaderStream << gShaderFile.rdbuf();
			gShaderFile.close();
			geometryCode = gShaderStream.str();
		}

		// Conditionally open and read Tessellation Control Shader file
		if (TCSPath != nullptr)
		{
			tcsShaderFile.open(TCSPath);
			std::stringstream tcsShaderStream;
			tcsShaderStream << tcsShaderFile.rdbuf();
			tcsShaderFile.close();
			TCSCode = tcsShaderStream.str();
		}

		// Conditionally open and read Tessellation Evaluation Shader file
		if (TESPath != nullptr)
		{
			tesShaderFile.open(TESPath);
			std::stringstream tesShaderStream;
			tesShaderStream << tesShaderFile.rdbuf();
			tesShaderFile.close();
			TESCode = tesShaderStream.str();
		}
	}
	catch (std::ifstream::failure e)
	{
		std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ: " << e.what() << std::endl; // Added e.what() for more detail
	}
	// Get C-style strings for OpenGL API calls
	const char* vShaderCode = vertexCode.c_str();
	const char* fShaderCode = fragmentCode.c_str();
	const char* gShaderCode = geometryPath != nullptr ? geometryCode.c_str() : nullptr;
	const char* tcsShaderCode = TCSPath != nullptr ? TCSCode.c_str() : nullptr;
	const char* tesShaderCode = TESPath != nullptr ? TESCode.c_str() : nullptr;

	// 2. Compile shaders
	unsigned int vertex, fragment, geometry, tessControl, tessEval;

	// Vertex Shader
	vertex = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertex, 1, &vShaderCode, NULL);
	glCompileShader(vertex);
	checkCompileErrors(vertex, "VERTEX"); // Custom error checking function

	// Fragment Shader
	fragment = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragment, 1, &fShaderCode, NULL);
	glCompileShader(fragment);
	checkCompileErrors(fragment, "FRAGMENT");

	// Geometry Shader (if provided)
	if (geometryPath != nullptr)
	{
		geometry = glCreateShader(GL_GEOMETRY_SHADER);
		glShaderSource(geometry, 1, &gShaderCode, NULL);
		glCompileShader(geometry);
		checkCompileErrors(geometry, "GEOMETRY");
		std::cout << "Loaded Geometry Shader" << std::endl; // More descriptive message
	}

	// Tessellation Control Shader (if provided)
	if (TCSPath != nullptr)
	{
		tessControl = glCreateShader(GL_TESS_CONTROL_SHADER);
		glShaderSource(tessControl, 1, &tcsShaderCode, NULL);
		glCompileShader(tessControl);
		checkCompileErrors(tessControl, "TESS_CONTROL");
		std::cout << "Loaded Tessellation Control Shader" << std::endl;
	}

	// Tessellation Evaluation Shader (if provided)
	if (TESPath != nullptr)
	{
		tessEval = glCreateShader(GL_TESS_EVALUATION_SHADER);
		glShaderSource(tessEval, 1, &tesShaderCode, NULL);
		glCompileShader(tesEval);
		checkCompileErrors(tesEval, "TESS_EVALUATION");
		std::cout << "Loaded Tessellation Evaluation Shader" << std::endl;
	}

	// Create and link shader Program
	ID = glCreateProgram();
	glAttachShader(ID, vertex);
	glAttachShader(ID, fragment);
	if (geometryPath != nullptr) glAttachShader(ID, geometry);
	if (TCSPath != nullptr) glAttachShader(ID, tessControl);
	if (TESPath != nullptr) glAttachShader(ID, tessEval);
	glLinkProgram(ID);
	checkCompileErrors(ID, "PROGRAM"); // Custom error checking for linking

	// Clean up: delete individual shaders after linking
	glDeleteShader(vertex);
	glDeleteShader(fragment);
	if (geometryPath != nullptr) glDeleteShader(geometry);
	if (TCSPath != nullptr) glDeleteShader(tessControl);
	if (TESPath != nullptr) glDeleteShader(tessEval);
}

2. Terrain::loadHeightMap: Sculpting Landscapes with Textures!

This snippet from my Terrain.cpp shows how I load a heightmap image and bind it as a 2D texture in OpenGL. This heightmap is then sampled in the tessellation evaluation shader to displace vertices, giving our flat terrain mesh its mountainous, detailed appearance. I also configure the texture parameters for proper wrapping and filtering.

2. Terrain::loadHeightMap: Sculpting Landscapes with Textures!

This snippet from my Terrain.cpp shows how I load a heightmap image and bind it as a 2D texture in OpenGL. This heightmap is then sampled in the tessellation evaluation shader to displace vertices, giving our flat terrain mesh its mountainous, detailed appearance. I also configure the texture parameters for proper wrapping and filtering.

2. Terrain::loadHeightMap: Sculpting Landscapes with Textures!

This snippet from my Terrain.cpp shows how I load a heightmap image and bind it as a 2D texture in OpenGL. This heightmap is then sampled in the tessellation evaluation shader to displace vertices, giving our flat terrain mesh its mountainous, detailed appearance. I also configure the texture parameters for proper wrapping and filtering.

#include "Terrain.h" // Includes relevant headers for Terrain class
#include <stb_image.h> // For loading image files
#include <iostream>

// ... other Terrain functions ...

// Loads a heightmap image and sets it up as an OpenGL texture
void Terrain::loadHeightMap(const char* path) {
    glGenTextures(1, &heightMapTexture);    // Generate a new texture ID
    glBindTexture(GL_TEXTURE_2D, heightMapTexture); // Bind it as a 2D texture

    // Load image data using stb_image
    int width, height, nrChannels;
    unsigned char* data = stbi_load(path, &width, &height, &nrChannels, 0);

    if (data) {
        GLenum format;
        // Determine the OpenGL internal format based on the number of channels in the image
        if (nrChannels == 1)
            format = GL_RED;   // Grayscale heightmap
        else if (nrChannels == 3)
            format = GL_RGB;   // RGB image
        else if (nrChannels == 4)
            format = GL_RGBA;  // RGBA image (with alpha)
        else {
            std::cerr << "Unsupported number of channels for height map: " << nrChannels << std::endl;
            stbi_image_free(data);
            return;
        }

        // Upload the image data to the GPU
        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D); // Generate mipmaps for distance-based detail
    }
    else {
        std::cout << "Failed to load height map texture at path: " << path << std::endl;
    }
    stbi_image_free(data); // Free the CPU-side image data

    // Set texture wrapping parameters to repeat (important for tiling terrain)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    // Set texture filtering parameters for smooth scaling
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); // Mipmap filtering
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Magnification filtering
}

// ... rest of Terrain.cpp ...

Have a project in mind?

Let's build something great. Whether it's complex gameplay mechanics, intelligent AI, or beautiful shaders, I'm ready to bring your vision to life.

Have a project in mind?

Let's build something great. Whether it's complex gameplay mechanics, intelligent AI, or beautiful shaders, I'm ready to bring your vision to life.

Have a project in mind?

Let's build something great. Whether it's complex gameplay mechanics, intelligent AI, or beautiful shaders, I'm ready to bring your vision to life.

© 2025 Portfolio

© 2025 Portfolio

Chaouki Mabrouki

Portfolio

Create a free website with Framer, the website builder loved by startups, designers and agencies.