Hands-On Graphical Programming Tutorial in C++ From OpenGL to DirectX 12
This tutorial walks you through modern graphical programming in C++, starting with a simple OpenGL triangle and progressing to advanced DirectX 12 Ultimate ray tracing. You’ll learn to set up tools, write shaders, and optimize rendering workflows. All code examples are designed for Windows 11 with up-to-date SDKs and frameworks.
Setting Up Your Development Environment
Tools Required:
Visual Studio 2025 Community Edition (with C++ and Game Development workloads).
vcpkg (C++ package manager).
CMake (for cross-platform builds).
RenderDoc (GPU debugger).
DirectX 12 SDK and Windows 11 SDK (included with Visual Studio).
Installation Steps:
1. Install Visual Studio 2025 and select "Desktop Development with C++" and "Game Development with C++" workloads.
2. Install vcpkg (follow [vcpkg.io](https://vcpkg.io)).
3. Install CMake from [cmake.org](https://cmake.org).
4. Download RenderDoc from [renderdoc.org](https://renderdoc.org).
Example 1: Your First OpenGL Triangle
Objective: Render a triangle using OpenGL 4.6.
Step 1: Set Up Dependencies
vcpkg install glfw3 glew glm # Install GLFW, GLEW, and GLM via vcpkg
Step 2: Create a CMake Project
- CMakeLists.txt:
cmake_minimum_required(VERSION 3.25)
project(Triangle)
find_package(glfw3 CONFIG REQUIRED)
find_package(GLEW REQUIRED) # Use "GLEW"
add_executable(Triangle main.cpp)
target_link_libraries(Triangle PRIVATE glfw GLEW::GLEW)
Step 3: Write the Code
- main.cpp:
#include <GL/glew.h>
#include <GLFW/glfw3.h>
int main() {
glfwInit();
GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL Triangle", nullptr, nullptr);
glfwMakeContextCurrent(window);
glewInit();
while (!glfwWindowShouldClose(window)) {
glClear(GL_COLOR_BUFFER_BIT);
glBegin(GL_TRIANGLES);
glColor3f(1, 0, 0); glVertex2f(-0.5, -0.5);
glColor3f(0, 1, 0); glVertex2f(0.5, -0.5);
glColor3f(0, 0, 1); glVertex2f(0.0, 0.5);
glEnd();
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}
Step 4: Build and Run
mkdir build && cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=[path/to/vcpkg/scripts/buildsystems/vcpkg.cmake]
cmake --build .
./Triangle
Debugging: Use RenderDoc to capture the frame and inspect the triangle.
Modern OpenGL with Shaders
Objective: Render a rotating 3D cube using vertex/fragment shaders.
Project Structure:
3dcube/
├── CMakeLists.txt
├── main.cpp
├── shaders/
│ ├── vertex.glsl
│ └── fragment.glsl
1. CMakeLists.txt
cmake_minimum_required(VERSION 3.25)
project(RotatingCube)
# Find required packages
find_package(glfw3 CONFIG REQUIRED)
find_package(GLEW REQUIRED)
find_package(GLM REQUIRED)
find_package(glad CONFIG REQUIRED)
# Configure shaders to be copied to build directory
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/shaders/vertex.glsl ${CMAKE_CURRENT_BINARY_DIR}/shaders/vertex.glsl COPYONLY)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/shaders/fragment.glsl ${CMAKE_CURRENT_BINARY_DIR}/shaders/fragment.glsl COPYONLY)
# Create executable
add_executable(RotatingCube main.cpp)
# Link libraries
target_link_libraries(RotatingCube PRIVATE
glfw
GLEW::GLEW
glm::glm
glad::glad
)
# Copy shaders to build directory on Windows
if(WIN32)
add_custom_command(TARGET RotatingCube POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/shaders/
<TARGET_FILE_DIR:RotatingCube>/3dcube/shaders/
)
endif()
NB : Change <TARGET_FILE_DIR:RotatingCube> with the path containing the project 3dcube
2. shaders/vertex.glsl
#version 460 core
layout(location = 0) in vec3 aPos;
uniform mat4 modelViewProjection;
void main()
{
gl_Position = modelViewProjection * vec4(aPos, 1.0);
}
3. shaders/fragment.glsl
#version 460 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0, 0.5, 0.2, 1.0);
}
4. main.cpp
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>
#include <fstream>
#include <sstream>
// Cube vertices
float vertices[] = {
// Positions
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
};
unsigned int indices[] = {
0, 1, 2, 2, 3, 0,
4, 5, 6, 6, 7, 4,
0, 4, 7, 7, 3, 0,
1, 5, 6, 6, 2, 1,
3, 2, 6, 6, 7, 3,
0, 1, 5, 5, 4, 0
};
GLuint loadShader(GLenum type, const char* path) {
// Load shader code from file
std::string code;
std::ifstream file;
file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try {
file.open(path);
std::stringstream stream;
stream << file.rdbuf();
file.close();
code = stream.str();
} catch (...) {
std::cerr << "Error reading shader file: " << path << std::endl;
return 0;
}
// Compile shader
const char* codePtr = code.c_str();
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &codePtr, NULL);
glCompileShader(shader);
// Check errors
GLint success;
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
char infoLog[512];
glGetShaderInfoLog(shader, 512, NULL, infoLog);
std::cerr << "Shader compilation error (" << path << "):\n" << infoLog << std::endl;
return 0;
}
return shader;
}
GLuint createShaderProgram(const char* vertexPath, const char* fragmentPath) {
GLuint vertexShader = loadShader(GL_VERTEX_SHADER, vertexPath);
GLuint fragmentShader = loadShader(GL_FRAGMENT_SHADER, fragmentPath);
GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
// Check linking errors
GLint success;
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
char infoLog[512];
glGetProgramInfoLog(program, 512, NULL, infoLog);
std::cerr << "Shader program linking error:\n" << infoLog << std::endl;
return 0;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return program;
}
int main() {
// Initialize GLFW
if (!glfwInit()) {
std::cerr << "Failed to initialize GLFW" << std::endl;
return -1;
}
// Configure GLFW
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// Create window
GLFWwindow* window = glfwCreateWindow(800, 600, "Rotating Cube", NULL, NULL);
if (!window) {
std::cerr << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
// Initialize GLAD
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
std::cerr << "Failed to initialize GLAD" << std::endl;
return -1;
}
// Create shader program
GLuint shaderProgram = createShaderProgram("shaders/vertex.glsl", "shaders/fragment.glsl");
if (!shaderProgram) return -1;
// Set up buffers
GLuint VAO, VBO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
// Enable depth testing
glEnable(GL_DEPTH_TEST);
// Render loop
while (!glfwWindowShouldClose(window)) {
// Clear screen
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Create transformations
glm::mat4 model = glm::rotate(glm::mat4(1.0f), (float)glfwGetTime(), glm::vec3(0.5f, 1.0f, 0.0f));
glm::mat4 view = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, -3.0f));
glm::mat4 projection = glm::perspective(glm::radians(45.0f), 800.0f/600.0f, 0.1f, 100.0f);
glm::mat4 mvp = projection * view * model;
// Use shader program
glUseProgram(shaderProgram);
GLint mvpLoc = glGetUniformLocation(shaderProgram, "modelViewProjection");
glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, glm::value_ptr(mvp));
// Draw cube
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, sizeof(indices)/sizeof(indices[0]), GL_UNSIGNED_INT, 0);
// Swap buffers and poll events
glfwSwapBuffers(window);
glfwPollEvents();
}
// Cleanup
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
glDeleteProgram(shaderProgram);
glfwTerminate();
return 0;
}
To Build and Run:
Install dependencies using vcpkg:
vcpkg install glfw3:x64-windows glad:x64-windows glm:x64-windows
Configure with CMake:
cd 3dcube
mkdir build
cd build
cmake -B build -DCMAKE_TOOLCHAIN_FILE=[path/to/vcpkg]/scripts/buildsystems/vcpkg.cmake
Build and run:
cd build
cmake --build . --config Release
xcopy ..\shaders Release /s /e /h (copy project shaders to RotatingCube.exe folder)
.\build\Release\RotatingCube.exe
DirectX 12 Triangle
Objective: Render a triangle using DirectX 12 Ultimate.
Step 1: Create a DirectX 12 Project
- In Visual Studio, create a new "Windows Desktop Wizard" project with C++.
Step 2: Initialize DirectX 12
- Use D3D12CreateDevice, create a command queue, and set up a swap chain.
Sample Code Snippet:
// Simplified setup (full example requires 200+ lines)
ComPtr<ID3D12Device> device;
D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_12_2, IID_PPV_ARGS(&device));
// Create command queue, swap chain, and pipeline state...
Step 3: Write HLSL Shaders
- shader.hlsl:
struct PSInput {
float4 position : SV_POSITION;
float4 color : COLOR;
};
PSInput VSMain(float3 position : POSITION) {
PSInput result;
result.position = float4(position, 1.0f);
result.color = float4(1.0f, 0.0f, 0.0f, 1.0f);
return result;
}
float4 PSMain(PSInput input) : SV_TARGET {
return input.color;
}
Debugging: Use PIX for Windows (included with the Windows SDK) to profile GPU workloads.
Advanced Example: Cross-Platform 3D with bgfx
Objective: Render a 3D model using bgfx, a cross-platform graphics library.
Step 1: Install bgfx
vcpkg install bgfx # Installs bgfx and dependencies
Step 2: Minimal bgfx Application
#include <bgfx/bgfx.h>
#include <bx/bx.h>
int main() {
bgfx::Init init;
init.platformData.nwh = glfwGetWin32Window(window); // Requires GLFW window
bgfx::init(init);
bgfx::setViewClear(0, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, 0x303030ff);
while (!glfwWindowShouldClose(window)) {
bgfx::frame();
}
bgfx::shutdown();
return 0;
}
Advanced Topic: Compute Shaders and Ray Tracing
Objective: Simulate particles using a compute shader and render with DirectX Raytracing (DXR).
DirectX 12 Ultimate Setup:
1. Enable DXR in your project settings.
2. Use DirectML for AI denoising (optional).
Sample Workflow:
- Write a compute shader to update particle positions.
- Use DXR to ray trace reflections.
Full Tutorial: Follow Microsoft’s [DirectX-Graphics-Samples](https://github.com/microsoft/DirectX-Graphics-Samples).
Tools to Test and Debug
1. RenderDoc: Inspect OpenGL/DirectX frames.
2. PIX for Windows: Profile DirectX 12 workloads.
3. NSight Graphics (NVIDIA) or Radeon GPU Profiler (AMD): Analyze GPU performance.
4. Visual Studio Graphics Debugger: Step through shaders.
Conclusion
By progressing from OpenGL basics to DirectX 12 ray tracing, you’ve gained hands-on experience with modern graphical programming. Experiment with cross-platform frameworks like bgfx, and explore AI-driven rendering techniques to stay ahead.
Next Steps:
- Explore Vulkan tutorials at [vulkan-tutorial.com].
- Clone full examples from [Microsoft’s Samples](https://github.com/microsoft/DirectX-Graphics-Samples).
Compile, tweak, and innovate—your journey into graphical engineering starts here!
#OpenGL #DirectX12