A CPU-based software renderer designed to explore low-level rendering techniques and principles. This project implements various rasterization features without relying on hardware acceleration, making it a great educational and experimentation project for graphics programming.
- Triangle Rasterization
- Line Rasterization
- Texture Mapping
- Clipping
- Mipmapping based on texture derivates
- MSAA
- Vertex pass
- Fragment pass
- Depth Writing
- Depth Testing
- Shadow Mapping
- OBJ Parsing
Clipping | Mipmapping | Texture Coordinates Derivates | Shadow Mapping |
---|---|---|---|
![]() |
![]() |
![]() |
![]() |
An OpenGL-inspired API that encapsulates its own Rendering State and manages internal buffers to provide a streamlined interface for Drawing, handling Buffers, and processing Shaders.
Initialize the AmberGL rendering system and configure the basic rendering state:
// Initialize the renderer with width and height
AmberGL::Initialize(800, 600);
// Configure multisampling for anti-aliasing
AmberGL::WindowHint(AGL_SAMPLES, 4); // 4x multisampling
AmberGL::Enable(AGL_MULTISAMPLE); // Enable multisampling
// Set up the rendering state
AmberGL::Enable(AGL_DEPTH_TEST); // Enable depth testing
AmberGL::Enable(AGL_DEPTH_WRITE); // Enable depth writing
AmberGL::Enable(AGL_CULL_FACE); // Enable face culling
AmberGL::CullFace(AGL_BACK); // Cull back faces
// Set viewport dimensions
AmberGL::Viewport(0, 0, 800, 600);
// Set clear color
AmberGL::ClearColor(0.2f, 0.3f, 0.3f, 1.0f);
Basic example of setting up a vertex buffer and drawing a triangle:
// Define triangle vertices
std::vector<Geometry::Vertex> vertices = {
Geometry::Vertex(glm::vec3(-0.5f, -0.5f, 0.0f), glm::vec2(0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
Geometry::Vertex(glm::vec3( 0.5f, -0.5f, 0.0f), glm::vec2(1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
Geometry::Vertex(glm::vec3( 0.0f, 0.5f, 0.0f), glm::vec2(0.5f, 1.0f), glm::vec3(0.0f, 0.0f, 1.0f))
};
// Create and bind a vertex array object
uint32_t VAO;
AmberGL::GenVertexArrays(1, &VAO);
AmberGL::BindVertexArray(VAO);
// Create and bind a vertex buffer
uint32_t VBO;
AmberGL::GenBuffers(1, &VBO);
AmberGL::BindBuffer(AGL_ARRAY_BUFFER, VBO);
AmberGL::BufferData(AGL_ARRAY_BUFFER, vertices.size() * sizeof(Geometry::Vertex), vertices.data());
// Create program and shader
uint32_t program = AmberGL::CreateProgram();
AmberGL::AttachShader(program, shaderInstance);
// Main rendering loop
while (running) {
// Clear the screen
AmberGL::Clear(AGL_COLOR_BUFFER_BIT | AGL_DEPTH_BUFFER_BIT);
// Calculate transformation matrices
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 view = camera.GetViewMatrix();
glm::mat4 projection = camera.GetProjectionMatrix();
// Set shader uniforms
shaderInstance->SetUniform("u_Model", model);
shaderInstance->SetUniform("u_View", view);
shaderInstance->SetUniform("u_Projection", projection);
// Use the program
AmberGL::UseProgram(program);
// Bind the VAO and draw
AmberGL::BindVertexArray(VAO);
AmberGL::DrawArrays(AGL_TRIANGLES, 0, 3);
AmberGL::BindVertexArray(0);
}
// Cleanup
AmberGL::DeleteProgram(program);
AmberGL::DeleteBuffers(1, &VBO);
AmberGL::DeleteVertexArrays(1, &VAO);
Optimize rendering by using index buffers:
// Define vertices and indices for a square (two triangles)
std::vector<Geometry::Vertex> vertices = {
Geometry::Vertex(glm::vec3(-0.5f, -0.5f, 0.0f), glm::vec2(0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
Geometry::Vertex(glm::vec3( 0.5f, -0.5f, 0.0f), glm::vec2(1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
Geometry::Vertex(glm::vec3( 0.5f, 0.5f, 0.0f), glm::vec2(1.0f, 1.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
Geometry::Vertex(glm::vec3(-0.5f, 0.5f, 0.0f), glm::vec2(0.0f, 1.0f), glm::vec3(0.0f, 0.0f, 1.0f))
};
std::vector<uint32_t> indices = {
0, 1, 2, // First triangle
2, 3, 0 // Second triangle
};
// Create and bind VAO
uint32_t VAO;
AmberGL::GenVertexArrays(1, &VAO);
AmberGL::BindVertexArray(VAO);
// Create and bind VBO
uint32_t VBO;
AmberGL::GenBuffers(1, &VBO);
AmberGL::BindBuffer(AGL_ARRAY_BUFFER, VBO);
AmberGL::BufferData(AGL_ARRAY_BUFFER, vertices.size() * sizeof(Geometry::Vertex), vertices.data());
// Create and bind EBO (Element Buffer Object)
uint32_t EBO;
AmberGL::GenBuffers(1, &EBO);
AmberGL::BindBuffer(AGL_ELEMENT_ARRAY_BUFFER, EBO);
AmberGL::BufferData(AGL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(uint32_t), indices.data());
// Create program and attach shader
uint32_t program = AmberGL::CreateProgram();
AmberGL::AttachShader(program, shaderInstance);
// During rendering:
// Set transformation matrices
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 view = camera.GetViewMatrix();
glm::mat4 projection = camera.GetProjectionMatrix();
// Set shader uniforms
shaderInstance->SetUniform("u_Model", model);
shaderInstance->SetUniform("u_View", view);
shaderInstance->SetUniform("u_Projection", projection);
// Use program and draw
AmberGL::UseProgram(program);
AmberGL::BindVertexArray(VAO);
AmberGL::DrawElements(AGL_TRIANGLES, indices.size());
AmberGL::BindVertexArray(0);
Create a textured 3D cube:
// Define cube vertices with positions, UVs, and normals
std::vector<Geometry::Vertex> cubeVertices = {
// Front face
Geometry::Vertex(glm::vec3(-1.0f, -1.0f, 1.0f), glm::vec2(0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
Geometry::Vertex(glm::vec3( 1.0f, -1.0f, 1.0f), glm::vec2(1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
Geometry::Vertex(glm::vec3( 1.0f, 1.0f, 1.0f), glm::vec2(1.0f, 1.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
Geometry::Vertex(glm::vec3(-1.0f, 1.0f, 1.0f), glm::vec2(0.0f, 1.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
// Back face
Geometry::Vertex(glm::vec3(-1.0f, -1.0f, -1.0f), glm::vec2(1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f)),
Geometry::Vertex(glm::vec3(-1.0f, 1.0f, -1.0f), glm::vec2(1.0f, 1.0f), glm::vec3(0.0f, 0.0f, -1.0f)),
Geometry::Vertex(glm::vec3( 1.0f, 1.0f, -1.0f), glm::vec2(0.0f, 1.0f), glm::vec3(0.0f, 0.0f, -1.0f)),
Geometry::Vertex(glm::vec3( 1.0f, -1.0f, -1.0f), glm::vec2(0.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f)),
// Top face
Geometry::Vertex(glm::vec3(-1.0f, 1.0f, -1.0f), glm::vec2(0.0f, 1.0f), glm::vec3(0.0f, 1.0f, 0.0f)),
Geometry::Vertex(glm::vec3(-1.0f, 1.0f, 1.0f), glm::vec2(0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f)),
Geometry::Vertex(glm::vec3( 1.0f, 1.0f, 1.0f), glm::vec2(1.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f)),
Geometry::Vertex(glm::vec3( 1.0f, 1.0f, -1.0f), glm::vec2(1.0f, 1.0f), glm::vec3(0.0f, 1.0f, 0.0f)),
// Bottom face
Geometry::Vertex(glm::vec3(-1.0f, -1.0f, -1.0f), glm::vec2(1.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
Geometry::Vertex(glm::vec3( 1.0f, -1.0f, -1.0f), glm::vec2(0.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
Geometry::Vertex(glm::vec3( 1.0f, -1.0f, 1.0f), glm::vec2(0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
Geometry::Vertex(glm::vec3(-1.0f, -1.0f, 1.0f), glm::vec2(1.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
// Right face
Geometry::Vertex(glm::vec3( 1.0f, -1.0f, -1.0f), glm::vec2(1.0f, 0.0f), glm::vec3(1.0f, 0.0f, 0.0f)),
Geometry::Vertex(glm::vec3( 1.0f, 1.0f, -1.0f), glm::vec2(1.0f, 1.0f), glm::vec3(1.0f, 0.0f, 0.0f)),
Geometry::Vertex(glm::vec3( 1.0f, 1.0f, 1.0f), glm::vec2(0.0f, 1.0f), glm::vec3(1.0f, 0.0f, 0.0f)),
Geometry::Vertex(glm::vec3( 1.0f, -1.0f, 1.0f), glm::vec2(0.0f, 0.0f), glm::vec3(1.0f, 0.0f, 0.0f)),
// Left face
Geometry::Vertex(glm::vec3(-1.0f, -1.0f, -1.0f), glm::vec2(0.0f, 0.0f), glm::vec3(-1.0f, 0.0f, 0.0f)),
Geometry::Vertex(glm::vec3(-1.0f, -1.0f, 1.0f), glm::vec2(1.0f, 0.0f), glm::vec3(-1.0f, 0.0f, 0.0f)),
Geometry::Vertex(glm::vec3(-1.0f, 1.0f, 1.0f), glm::vec2(1.0f, 1.0f), glm::vec3(-1.0f, 0.0f, 0.0f)),
Geometry::Vertex(glm::vec3(-1.0f, 1.0f, -1.0f), glm::vec2(0.0f, 1.0f), glm::vec3(-1.0f, 0.0f, 0.0f))
};
// Define cube indices
std::vector<uint32_t> cubeIndices = {
0, 1, 2, 2, 3, 0, // Front face
4, 5, 6, 6, 7, 4, // Back face
8, 9, 10, 10, 11, 8, // Top face
12, 13, 14, 14, 15, 12, // Bottom face
16, 17, 18, 18, 19, 16, // Right face
20, 21, 22, 22, 23, 20 // Left face
};
// Create and bind a vertex array object
uint32_t cubeVAO;
AmberGL::GenVertexArrays(1, &cubeVAO);
AmberGL::BindVertexArray(cubeVAO);
// Create and bind a vertex buffer
uint32_t cubeVBO;
AmberGL::GenBuffers(1, &cubeVBO);
AmberGL::BindBuffer(AGL_ARRAY_BUFFER, cubeVBO);
AmberGL::BufferData(AGL_ARRAY_BUFFER, cubeVertices.size() * sizeof(Geometry::Vertex), cubeVertices.data());
// Create and bind element buffer object
uint32_t cubeEBO;
AmberGL::GenBuffers(1, &cubeEBO);
AmberGL::BindBuffer(AGL_ELEMENT_ARRAY_BUFFER, cubeEBO);
AmberGL::BufferData(AGL_ELEMENT_ARRAY_BUFFER, cubeIndices.size() * sizeof(uint32_t), cubeIndices.data());
// Create and load a texture
uint32_t texture;
AmberGL::GenTextures(1, &texture);
AmberGL::BindTexture(AGL_TEXTURE_2D, texture);
// Set texture parameters
AmberGL::TexParameteri(AGL_TEXTURE_2D, AGL_TEXTURE_MIN_FILTER, AGL_LINEAR);
AmberGL::TexParameteri(AGL_TEXTURE_2D, AGL_TEXTURE_MAG_FILTER, AGL_LINEAR);
AmberGL::TexParameteri(AGL_TEXTURE_2D, AGL_TEXTURE_WRAP_S, AGL_REPEAT);
AmberGL::TexParameteri(AGL_TEXTURE_2D, AGL_TEXTURE_WRAP_T, AGL_REPEAT);
// Load texture data
uint8_t* data;
uint32_t width, height;
loadTextureData("texture.png", data, width, height);
AmberGL::TexImage2D(AGL_TEXTURE_2D, 0, AGL_RGBA8, width, height, 0, AGL_RGBA8, AGL_UNSIGNED_BYTE, data);
AmberGL::GenerateMipmap(AGL_TEXTURE_2D);
// Create program and attach shader
uint32_t cubeProgram = AmberGL::CreateProgram();
AmberGL::AttachShader(cubeProgram, cubeShaderInstance);
// Rendering loop
while (running) {
AmberGL::Clear(AGL_COLOR_BUFFER_BIT | AGL_DEPTH_BUFFER_BIT);
// Model matrix - static cube
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 view = camera.GetViewMatrix();
glm::mat4 projection = camera.GetProjectionMatrix();
// Set shader uniforms
cubeShaderInstance->SetUniform("u_Model", model);
cubeShaderInstance->SetUniform("u_View", view);
cubeShaderInstance->SetUniform("u_Projection", projection);
cubeShaderInstance->SetUniform("u_DiffuseMap", 0); // Texture unit 0
cubeShaderInstance->SetUniform("u_ViewPos", cameraPosition);
// Use the program
AmberGL::UseProgram(cubeProgram);
// Bind texture
AmberGL::ActiveTexture(AGL_TEXTURE0);
AmberGL::BindTexture(AGL_TEXTURE_2D, texture);
// Draw the cube
AmberGL::BindVertexArray(cubeVAO);
AmberGL::DrawElements(AGL_TRIANGLES, cubeIndices.size());
AmberGL::BindVertexArray(0);
}
// Cleanup
AmberGL::DeleteTextures(1, &texture);
AmberGL::DeleteBuffers(1, &cubeVBO);
AmberGL::DeleteBuffers(1, &cubeEBO);
AmberGL::DeleteVertexArrays(1, &cubeVAO);
AmberGL::DeleteProgram(cubeProgram);
Creating and using framebuffers for post-processing effects:
// Create a framebuffer object for off-screen rendering
uint32_t FBO;
AmberGL::GenFramebuffers(1, &FBO);
AmberGL::BindFramebuffer(AGL_FRAMEBUFFER, FBO);
// Create a texture for the color attachment
uint32_t colorTexture;
AmberGL::GenTextures(1, &colorTexture);
AmberGL::BindTexture(AGL_TEXTURE_2D, colorTexture);
AmberGL::TexImage2D(AGL_TEXTURE_2D, 0, AGL_RGBA8, 1024, 1024, 0, AGL_RGBA8, AGL_UNSIGNED_BYTE, nullptr);
AmberGL::TexParameteri(AGL_TEXTURE_2D, AGL_TEXTURE_MIN_FILTER, AGL_LINEAR);
AmberGL::TexParameteri(AGL_TEXTURE_2D, AGL_TEXTURE_MAG_FILTER, AGL_LINEAR);
AmberGL::FramebufferTexture2D(AGL_FRAMEBUFFER, AGL_COLOR_ATTACHMENT, AGL_TEXTURE_2D, colorTexture, 0);
// Create a renderbuffer object for depth testing
uint32_t RBO;
AmberGL::GenRenderbuffers(1, &RBO);
AmberGL::BindRenderbuffer(AGL_RENDERBUFFER, RBO);
AmberGL::RenderbufferStorage(AGL_RENDERBUFFER, AGL_DEPTH_COMPONENT, 1024, 1024);
AmberGL::FramebufferRenderbuffer(AGL_FRAMEBUFFER, AGL_DEPTH_ATTACHMENT, AGL_RENDERBUFFER, RBO);
// Unbind the framebuffer
AmberGL::BindFramebuffer(AGL_FRAMEBUFFER, 0);
// Create programs for scene rendering and post-processing
uint32_t sceneProgram = AmberGL::CreateProgram();
AmberGL::AttachShader(sceneProgram, sceneShaderInstance);
uint32_t postProcessProgram = AmberGL::CreateProgram();
AmberGL::AttachShader(postProcessProgram, postProcessShaderInstance);
// Prepare post-process quad vertices
std::vector<Geometry::Vertex> quadVertices = {
Geometry::Vertex(glm::vec3(-1.0f, -1.0f, 0.0f), glm::vec2(0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
Geometry::Vertex(glm::vec3(-1.0f, 1.0f, 0.0f), glm::vec2(0.0f, 1.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
Geometry::Vertex(glm::vec3( 1.0f, -1.0f, 0.0f), glm::vec2(1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
Geometry::Vertex(glm::vec3( 1.0f, 1.0f, 0.0f), glm::vec2(1.0f, 1.0f), glm::vec3(0.0f, 0.0f, 1.0f))
};
// Create and bind VAO for quad
uint32_t quadVAO;
AmberGL::GenVertexArrays(1, &quadVAO);
AmberGL::BindVertexArray(quadVAO);
// Create VBO for quad
uint32_t quadVBO;
AmberGL::GenBuffers(1, &quadVBO);
AmberGL::BindBuffer(AGL_ARRAY_BUFFER, quadVBO);
AmberGL::BufferData(AGL_ARRAY_BUFFER, quadVertices.size() * sizeof(Geometry::Vertex), quadVertices.data());
AmberGL::BindVertexArray(0);
// Main rendering loop
while (running) {
// ======== First pass: Render scene to framebuffer ========
AmberGL::BindFramebuffer(AGL_FRAMEBUFFER, FBO);
AmberGL::Viewport(0, 0, 1024, 1024);
AmberGL::Clear(AGL_COLOR_BUFFER_BIT | AGL_DEPTH_BUFFER_BIT);
// Setup scene rendering
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 view = camera.GetViewMatrix();
glm::mat4 projection = camera.GetProjectionMatrix();
// Set scene shader uniforms
sceneShaderInstance->SetUniform("u_Model", model);
sceneShaderInstance->SetUniform("u_View", view);
sceneShaderInstance->SetUniform("u_Projection", projection);
sceneShaderInstance->SetUniform("u_DiffuseMap", 0);
// Use scene program and bind textures
AmberGL::UseProgram(sceneProgram);
AmberGL::ActiveTexture(AGL_TEXTURE0);
AmberGL::BindTexture(AGL_TEXTURE_2D, sceneTexture);
// Render scene objects
AmberGL::BindVertexArray(sceneVAO);
AmberGL::DrawElements(AGL_TRIANGLES, sceneIndexCount);
AmberGL::BindVertexArray(0);
// ======== Second pass: Apply post-processing effect ========
AmberGL::BindFramebuffer(AGL_FRAMEBUFFER, 0);
AmberGL::Viewport(0, 0, 800, 600);
AmberGL::Clear(AGL_COLOR_BUFFER_BIT);
// Set post-processing uniforms
float time = GetCurrentTime(); // Get current time for animated effects
postProcessShaderInstance->SetUniform("u_ScreenTexture", 0);
postProcessShaderInstance->SetUniform("u_Time", time);
postProcessShaderInstance->SetUniform("u_Effect", 1); // 1 = grayscale, 2 = invert, 3 = blur, etc.
// Use post-processing program
AmberGL::UseProgram(postProcessProgram);
// Bind the framebuffer's color texture
AmberGL::ActiveTexture(AGL_TEXTURE0);
AmberGL::BindTexture(AGL_TEXTURE_2D, colorTexture);
// Render the quad with post-processing effect applied
AmberGL::BindVertexArray(quadVAO);
AmberGL::DrawArrays(AGL_TRIANGLE_STRIP, 0, 4);
AmberGL::BindVertexArray(0);
}
// Cleanup resources
AmberGL::DeleteTextures(1, &colorTexture);
AmberGL::DeleteRenderbuffers(1, &RBO);
AmberGL::DeleteFramebuffers(1, &FBO);
AmberGL::DeleteBuffers(1, &quadVBO);
AmberGL::DeleteVertexArrays(1, &quadVAO);
AmberGL::DeleteProgram(sceneProgram);
AmberGL::DeleteProgram(postProcessProgram);
-
Rendering Enhancements:
- Renderer:
The current Renderer is minimal; to enhance modularity and extensibility, a rework is required to support a stateful pipeline and accommodate multiple render passes. - Anti-aliasing:
Refine the existing MSAA implementation and explore additional multi-sampling techniques (e.g., FXAA, SMAA) to effectively reduce aliasing artifacts while maintaining performance. - Mipmapping:
Optimize the mipmap generation algorithm for improved texture sampling and memory efficiency, and rework the adaptive level-of-detail (LOD) system to increase realtime performance and visual quality. - Shadow Mapping:
Implement a robust shadow mapping pipeline that addresses common challenges (e.g., shadow acne, depth biasing) to achieve dynamic shadows.
- Renderer:
-
UI Integration:
Implement a dedicated Panel to allow realtime control over:- Rasterization settings
- Render passes
- Scene management
...
-
Global Optimization:
- SIMD
- Tile Based Rasterization
- Visual Studio 2022
- SDL2 (Windowing and inputs)
- GLM (Mathematics)
- stb_image (Image Loader)
- Premake5 (Project generation)
Premake5 is used to generate project files.
To generate the project, execute GenerateProject.bat
. By default, GenerateProject.bat
will generate project files for Visual Studio 2022. If you want to use another version of Visual Studio you can execute GenerateProject.bat
from the command line with the Visual Studio version as argument. (ex: .\GeneratedProject.bat vs2019
)
Sponza rendered with shadow mapping and MSAA x16
Texture Filtering
Left Linear, Right Nearest.
Mipmapping
Left mipmaps off, Right mipmaps on.
Clipping
Frustum plane distances have been reduced to highlight clipping
This project is licenced under an MIT Licence.