A two-phase graphics project: a custom OpenGL rendering engine (Phase 1) and a 3D game built on top of it (Phase 2).
- Overview
- Goals
- Key Features
- Architecture
- Scene File Format
- Materials & Shaders
- Post-processing & Sky
- Lighting Model
- Collision & Physics
- Build & Run
- Project Structure (recommended)
- How to Add/Change Scenes and Assets
- Extending the Engine
- Performance Tips
- Testing & Debugging
- Known Limitations
- License
- Credits & References
- Contact
This repository contains a custom OpenGL-based rendering engine implemented in C++ with CMake, followed by a simple 3D game built on top of that engine. The engine is designed around an ECS (Entity-Component-System) architecture and supports scene deserialization from external data files, forward rendering, configurable pipeline state, post-processing, real-time lighting, textured materials, and basic collision handling.
The project is structured as a learning-focused engine that can be used as the basis for any simple 3D game and demonstrates low-level graphics concepts and engine patterns.
- Implement an engine able to deserialize scenes from external files.
- Support assets (shaders, textures, meshes) managed by singleton asset loaders.
- Provide an ECS-based runtime with components (e.g.,
MeshRenderer,Camera) and systems (e.g.,ForwardRenderer). - Support configurable rendering pipeline state per material (culling, depth test, blending, masks).
- Implement post-processing and sky rendering hooks.
- Build a 3D game using the engine.
- Use real 3D models, textures, multiple dynamic lights, and at least one postprocessing effect.
- Implement collision detection and basic game logic / objectives.
- Ensure all scenes are externally defined and loadable without recompilation.
- ECS architecture with easily-extendable components and systems
- Scene deserialization: assets, entities, components, and pipeline configured via data files
- Asset management: reusable assets (meshes, materials, shaders, textures, samplers)
- Forward rendering pipeline with correct opaque/transparent sorting
- Per-material pipeline state (back-face culling, depth testing, blending, color/depth mask)
- Post-processing pipeline support (framebuffer-based effects)
- Lighting: multiple light types supported and combined in shaders
- Lit material supporting standard texture maps (albedo, specular, roughness, AO, emission)
- Camera component with perspective/orthographic modes and scene graph transform hierarchy
- Collision detection & basic physics integration (external physics engine allowed)
- Modular design: add new components and systems with minimal coupling
- World: container for entities (a linear list or other container)
- Entity: lightweight ID + name, parent pointer, local transform; holds a list of components
- Component: abstract base class with virtual methods (e.g.,
deserialize(),update()) - Systems: process entities that have required components (e.g.,
ForwardRendererlooks forCameraandMeshRenderer)
Common components:
Transform— local transform and parent-child relationshipMeshRenderer— references to mesh and materialCamera— perspective/orthographic parametersLight— color, type, cone angles (position and direction come from Transform)Collider— collider shape data for collision detection
Design principles:
- Components should be serializable/deserializable
- Assets referenced by components are loaded through asset loaders by name
- Systems should not own components; they query the world and operate on component sets
- Singleton-like loaders (or centralized
AssetManager) map string names to loaded objects - Asset types:
ShaderProgram,Mesh,Texture,Sampler,Material - Loaders are responsible for file IO, GPU resource creation, and caching
- Asset deserialization: scene file lists assets by name and path; loaders construct the runtime objects
- The forward renderer loops over entities to find the main camera(s) and drawable entities
- Rendering order: opaque objects first (any order), then sky, then transparent objects sorted back-to-front
- Each
Materialincludes aPipelineStatestruct describing culling, depth testing, blending, masks - Rendering steps:
- Bind shader
- Set shader uniforms (camera matrices, material params, lights)
- Bind textures & samplers
- Configure pipeline state (glEnable/glDisable / glDepthFunc / glBlendFunc etc.)
- Draw mesh
All scene data lives in external files so scenes can be changed without recompilation. The example below uses JSON, but YAML or a custom format is also acceptable.
- Assets: shaders, meshes, textures, samplers, materials (with pipeline state)
- Entities: list of entities with name, parent, transform, and a list of components
- Systems: optional, to configure system-level settings (e.g., post-processing chain)
- Scene metadata: sky settings, default camera name, exposure, etc.
{
"assets": {
"shaders": {
"basic": "assets/shaders/basic.glsl",
"pbr": "assets/shaders/pbr.glsl"
},
"meshes": {
"cube": "assets/models/cube.obj",
"environment": "assets/models/scene.obj"
},
"textures": {
"sky": "assets/textures/sky.png"
},
"materials": {
"wall": {
"type": "lit",
"shader": "pbr",
"textures": {"albedo": "assets/textures/wall_albedo.png"},
"pipeline": {"cull": "back", "depth_test": true}
}
}
},
"entities": [
{
"name": "MainCamera",
"components": [
{"type": "Transform", "position": [0,2,5]},
{"type": "Camera", "fov": 60, "near": 0.1, "far": 100}
]
},
{
"name": "Room",
"components": [
{"type": "Transform"},
{"type": "MeshRenderer", "mesh": "environment", "material": "wall"}
]
}
],
"postprocessing": ["bloom"]
}Keep scene files human-editable. Asset names referenced by components should match keys in the assets section.
Materialis an abstract class. Subclasses include:TintedMaterial— uniform color, no texturesTexturedMaterial— base color textureLitMaterial— supports PBR textures (albedo, normal, roughness, metalness, AO, emission)
- Each material contains a
PipelineStateand references to required textures and the shader program. - Shader uniform conventions: standard names for matrices and material maps (e.g.,
u_ViewProj,u_Model,u_Albedo)
- Post-processing uses a framebuffer object (FBO) pipeline: render scene into texture(s), then process via full-screen quad passes.
- Common effects to include: tone mapping, bloom, gamma correction, screen-space ambient occlusion (SSAO), color grading.
- Sky is implemented as a textured skybox or skydome with optional shader for atmospheric scattering.
- Light component stores light parameters (color, intensity, type-specific params) excluding position/direction which are derived from entity transform.
- Support multiple light types: directional, point, spot.
- Shaders accumulate multiple lights per-object; consider a reasonable upper limit or use light culling for many lights.
- Lit material shader should combine textures and lighting using a physically-based shading model or a simplified Blinn-Phong model depending on scope.
- Collision detection (ray-picking, AABB/OBB, sphere) is required for the game phase. You may integrate an external physics library (e.g., Bullet) for rigid-body physics and collision resolution.
Collidercomponents hold collision shape data and optional physics parameters (mass, friction).- Implement simple collision callbacks or query APIs for game logic (e.g.,
onCollide,raycastFromCamera).
- C++17 or newer compiler
- CMake (>= 3.10)
- OpenGL development headers (GL 3.3+ ideally)
- Libraries (suggested):
- GLFW (window & input)
- GLAD or GLEW (OpenGL loader)
- GLM (math)
- stb_image (image loading) or equivalent
- Assimp (model loading)
- Optional: Bullet (physics), Dear ImGui (debug UI)
You can choose alternative libraries if preferred; adjust
CMakeLists.txtaccordingly.
mkdir build && cd build
cmake ..
cmake --build . --config ReleaseOn success, the engine executable(s) will be in build/bin (or as configured by CMakeLists.txt).
./bin/engine --scene ../scenes/example_scene.jsonProvide an argument to point to a scene file. The engine should fallback to a default scene if the argument is omitted.
/ (repo root)
├─ CMakeLists.txt
├─ README.md
├─ src/
│ ├─ core/ # ECS core, world, entity, component base
│ ├─ systems/ # ForwardRenderer, PhysicsSystem, etc.
│ ├─ components/ # MeshRenderer, Camera, Light, Collider
│ ├─ assets/ # Asset loader implementations
│ ├─ platform/ # Window and input (GLFW glue)
│ └─ app/ # main, application loop, CLI
├─ shaders/
├─ assets/
│ ├─ models/
│ ├─ textures/
│ └─ scenes/
├─ examples/ # example scenes and helper tools
└─ third_party/ # external libs (optional)
- Add files to
assets/models,assets/textures, orshaders/. - Update or create a new scene JSON file in
assets/scenes/referencing those assets. - Run the engine with
--scene path/to/scene.json.
Make sure asset keys in the scene file match loader keys.
- New Component: derive from
Component, implementdeserialize()and relevant update hooks; register so the deserializer can instantiate by type name. - New System: add a system class that queries the world for component sets and performs updates. Register it in the application initialization.
- New Material/Shader: create a shader, add a material subclass if needed, and add an entry in the scene
assets.materialssection. - Renderer: implement additional render passes (deferred, shadow maps, etc.) but ensure the scene format or a config option can enable them.
- Batch draw calls when possible by grouping by shader/material.
- Minimize state changes (textures, shader binds) during rendering.
- Use indexed VBOs/VAOs and static/dynamic usage hints appropriately.
- Use texture atlases where helpful and normal map compression for large textures.
- Profile with tools like RenderDoc and platform profilers.
- Add an in-engine debug UI (Dear ImGui) for live tweaking of cameras, lights, and material parameters.
- Validate scene deserialization with unit tests or small Python scripts.
- Provide verbose logging for asset loading failures and shader compile errors.
- Include debug draw modes (wireframe, normals visualization, bounding boxes).
- The engine is intended as an educational/simple engine; it is not optimized for AAA-scale performance.
- Light count is limited by the shader strategy; large numbers of dynamic lights will require light-culling or deferred approaches.
- Asset loaders assume common file formats (OBJ, glTF via Assimp); custom formats require additional parsers.
This repository is released under the MIT License. See LICENSE for details.
- Third-party libraries: GLFW, GLAD, GLM, stb_image, Assimp, Bullet, Dear ImGui
- Research & tutorials used for reference: OpenGL docs, ECS design patterns, PBR/lighting references