A real-time 3D game engine written in modern C++17, built for learning the internals of a renderer/ECS/physics stack — inspired by Hazel. It ships with Sandbox, a full editor application (ImGui-based) for building, editing, playing, and serializing scenes.
⚠️ This project is under active development.
- Highlights
- Feature Reference
- Built-in Showcases
- Components at a Glance
- Building
- Project Structure
- Tech Stack
- Resources
- Entity Component System powered by EnTT.
- Forward renderer (OpenGL core with GLSL ES 3.00 shaders) with a geometry-agnostic submission API.
- PBR-ish Blinn–Phong lighting: global ambient, one directional light, up to four point lights, per-material emissive/reflectivity.
- Directional shadow mapping with hardware PCF for smooth, unbanded soft shadows.
- HDR bloom / neon glow pipeline with ACES tonemapping and exposure control.
- Procedural environment reflections (sky/ground) per material.
- Skeletal animation — GPU vertex skinning, loads rigged/animated models (FBX, glTF, Collada, …) via Assimp.
- Rigid-body physics via Jolt: box, convex-hull, triangle-mesh and heightfield colliders.
- Editable terrain with a Blender-style sculpt brush (raise / lower / smooth).
- Animated transparent water surfaces for lakes/oceans.
- Lua scripting with an in-editor, syntax-highlighted code editor that runs while playing.
- Scene serialization (YAML) plus snapshot-based undo/redo.
- Full editor: hierarchy, properties inspector, gizmos, click-to-pick, play/stop, asset import.
Scenes are EnTT registries; an Entity is a lightweight handle wrapping an
entt::entity plus a Scene*. Components are plain structs in
Component.h. System logic runs inline in
Scene::OnUpdate. Create entities from code (Scene::CreateEntity) or from the editor
hierarchy panel.
The renderer (renderer.cpp) exposes a
geometry-agnostic path:
Renderer::BeginScene(projection, view, cameraPos);
Renderer::SetLights(ambient, dirExists, dirLight, dirDir, pointLights, pointPos, count);
Renderer::Submit(vertexArray, material, transform, diffuseMap); // any indexed mesh
Renderer::SubmitAnimated(vertexArray, material, transform, boneMatrices, diffuseMap);
Renderer::EndScene();Vertices are interleaved position / normal / texcoord / boneIDs / weights. The
renderer also draws editor aids: selection outlines (stencil-based), camera frustum
gizmos, and directional-light arrows. A global wireframe toggle is available
per entity via the WireframeComponent.
- Ambient: a single global ambient colour (editable in Settings, serialized).
- Directional light (
DirLightComponent): the "sun"; its aim is the entity's local −Z, visualized with an arrow gizmo. Casts shadows. - Point lights (
PointLightComponent): up to 4, with constant/linear/quadratic attenuation. - Emissive objects also emit light: any material with
emissiveStrength > 0is added as a gentle coloured point light onto its surroundings (until the 4-light cap). - Shadows: multi-light directional shadow mapping — up to 4 directional lights,
each with its own 2048² depth map rendered from its POV and sampled with hardware PCF
(
sampler2DShadow) for smooth edges. Toggle per light with Casts Shadow. Animated meshes are skinned in the shadow pass too, so a walking character casts a correctly posed shadow. (Point lights light the scene but don't cast shadows yet.)
An HDR pipeline (Bloom.cpp) renders the scene to an
RGBA16F target, extracts bright pixels (threshold), blurs them through a 10-pass
ping-pong Gaussian, then composites with ACES tonemapping and an exposure control.
Push a material's emissive * emissiveStrength above 1.0 to make it bloom — the basis of
the neon-glow look. All knobs (intensity, threshold, exposure) live in the editor Settings
panel.
Models are loaded with Assimp (Mesh.cpp) and support
diffuse textures. Loading is asynchronous: the file is parsed on a worker thread and
the GPU buffers/textures are uploaded on the main thread in Model::Update(), so the editor
never blocks on a slow import. Add via the editor (Mesh component) or CreateRef<Model>(path).
Full GPU vertex skinning (Animation.h /
Animation.cpp):
- The model loader extracts the bone hierarchy, per-vertex bone weights (up to 4), and all embedded animation clips.
Boneinterpolates keyframes (lerp position/scale, slerp rotation);Animatorwalks the skeleton each frame and produces up toMAX_BONES(100) final matrices.- The vertex shader blends the bone matrices by weight (
u_BoneMatrices[], gated byu_Animated), in both the lit pass and the shadow pass. - Drive it with an
AnimationComponent(clip,playing,speed).
Works with any rigged + animated file Assimp can read (FBX, glTF/GLB, Collada/DAE, …). See Built-in Showcases for the walking-robot demo and how to download a free rigged character + walk cycle from Mixamo.
Built-in procedural meshes (Primitives.cpp) —
Cube, Sphere, Cylinder (pillar), Cone, Plane — exposed through a PrimitiveComponent
(type + material). Meshes are unit-sized and cached/shared across entities. They render with
the full material pipeline (emissive, reflectivity, wireframe), cast shadows, show selection
outlines, and (with a Rigid Body + Mesh Collider) collide as convex hulls or triangle meshes.
Create from the hierarchy panel's Create Shape menu or Add Component → Shape.
Rigid-body simulation via Jolt Physics (pimpl wrapper in
Physics):
RigidBodyComponent— Static / Dynamic / Kinematic.- Colliders: Box (
BoxColliderComponent), Convex hull & triangle mesh (MeshColliderComponent, from the entity's mesh/primitive geometry), and a heightfield collider auto-generated for terrain. - Press Play to start the simulation; transforms are written back to entities every frame. Press Stop to restore the edit-time state.
TerrainComponent is an editable heightmap (resolution × resolution grid) rendered as a mesh
with analytic normals and an optional static triangle-mesh collider. A Blender-style
sculpt tool (raise / lower / smooth brush, configurable radius & strength) lets you carve
hills, valleys, and riverbeds directly in the viewport.
WaterComponent is an animated, transparent surface (a flat grid displaced by a sum-of-sines
wave in the shader) with Fresnel environment reflection, sun specular, and configurable
colour/alpha/amplitude/wave-scale/speed. Place one over a sculpted terrain basin to make a
lake.
LuaScriptComponent holds a Lua source string that runs while the scene is playing
(per-entity lua_State). Scripts can read/modify their entity — e.g. GetTranslation(),
SetTranslation(x,y,z), SetColor(r,g,b) — for movement, colour cycling, etc. The editor
includes a dedicated, syntax-highlighted code editor window (Lua keyword/API colouring,
Tab autocompletion for built-in functions, manual caret rendering) so you can edit scripts
without leaving the engine.
Scenes serialize to YAML (.phx) via SceneSerializer — every component, including
materials, terrain heightfields, water, primitives, lights, physics, animation config and
Lua source. The same machinery powers snapshot-based undo/redo, which reconciles the
live scene in place (so undoing an unrelated edit never reloads your meshes).
The Sandbox app provides:
- Hierarchy panel — create/delete/select entities; Create menu for lights, terrain, water, and shapes.
- Properties inspector — per-component panels with live editing.
- Viewport — ImGuizmo transform gizmos, click-to-pick selection, terrain sculpting.
- Settings — ambient, bloom/exposure, sculpt brush, vsync.
- File / Edit menus — load showcases, import/export scenes & shaders, undo/redo.
Controls
| Action | Binding |
|---|---|
| Translate / Rotate / Scale gizmo | W / E / R |
| No gizmo | Q |
| Orbit camera | Right-mouse drag |
| Pan camera | Middle-mouse drag |
| Zoom | Mouse wheel |
| Select entity | Left-click in viewport |
| Undo / Redo | Ctrl+Z / Ctrl+Shift+Z |
| Play / Stop | toolbar button |
Open via the File menu:
- Load Showcase Scene — bloom, neon glow, emissive lighting, environment reflection, shadows, a textured backpack dropping with a convex collider, and a Lua-scripted cube.
- Load Water Showcase — a sculpted terrain lake basin filled with animated water and glowing buoys.
- Load Robot Showcase — skeletal animation: a rigged character walking on a shadow-receiving floor.
The robot showcase loads assets/models/robot.fbx. To get a free rigged model + walk cycle:
- Sign in at mixamo.com (free Adobe account).
- Pick a rigged character (e.g. X Bot) or upload your own (it auto-rigs).
- Animations tab → search "Walking" → choose a walk cycle.
- Download as FBX Binary, Skin = With Skin (bundles mesh + skeleton + animation), 30 FPS. Keep In Place on so it walks without drifting.
- Rename to
robot.fbx, drop it inSandbox/assets/models/, rebuild (or copy intobuild/Sandbox/assets/models/), then File → Load Robot Showcase.
glTF (.glb), Collada (.dae) and Sketchfab "with animation" exports also work. Full notes
in Sandbox/assets/models/README.txt.
| Component | Purpose |
|---|---|
TagComponent |
Entity name |
TransformComponent |
Translation / rotation / scale |
CameraComponent |
Scene camera (perspective/ortho), primary flag |
MeshComponent |
Assimp-loaded model + material |
AnimationComponent |
Skeletal animation playback (clip / playing / speed) |
PrimitiveComponent |
Built-in shape (cube/sphere/cylinder/cone/plane) + material |
CubeComponent |
Built-in unit cube + material |
Material |
Ambient/diffuse/specular/shininess + emissive + reflectivity |
DirLightComponent |
Directional light (sun) |
PointLightComponent |
Point light with attenuation |
RigidBodyComponent |
Static / Dynamic / Kinematic body |
BoxColliderComponent |
Box collider |
MeshColliderComponent |
Convex-hull / triangle-mesh collider |
TerrainComponent |
Editable heightmap terrain |
WaterComponent |
Animated transparent water surface |
WireframeComponent |
Draw the entity as wireframe |
LuaScriptComponent |
Lua script run while playing |
NativeScriptComponent |
C++ scriptable entity |
Phoenix targets Linux.
Install via your system package manager (apt names shown):
sudo apt install build-essential cmake \
libglfw3-dev libglew-dev libglm-dev libopengl-dev \
libfmt-dev libspdlog-dev libassimp-dev libyaml-cpp-dev liblua5.3-dev- Jolt Physics is fetched and built from source automatically by CMake (FetchContent).
- EnTT is vendored as a single header in
third_party/. - ImGui / ImGuizmo are vendored in the engine sources.
mkdir -p build && cd build
cmake ..
cmake --build . -j$(nproc)
# Run the editor
./Sandbox/SandboxAssets (shaders, models, textures) are copied next to the executable on every build, so edits to shaders/scenes are picked up automatically on the next launch.
Phoenix/
├── phoenix/ # the engine (static library)
│ ├── include/Phoenix/ # public headers
│ │ ├── core/ event/ renderer/ Scene/ Physics/ imGui/ Math/ opengl/
│ └── src/ # implementation (renderer, Scene, Physics, ...)
├── Sandbox/ # the editor application
│ ├── MainLayer.cpp # viewport, showcases, gizmos, undo/redo, menus
│ ├── Panels/SceneEditor.* # hierarchy + properties + Lua editor panels
│ └── assets/ # shaders, models, textures (copied next to the exe)
├── Example/ # a minimal example app
├── third_party/entt/ # vendored EnTT single header
├── images/ # screenshots
└── CMakeLists.txt
| Area | Library |
|---|---|
| Windowing / input | GLFW |
| GL loading | GLEW |
| Math | GLM |
| ECS | EnTT (vendored) |
| Physics | Jolt Physics (fetched) |
| Model import | Assimp |
| Scripting | Lua 5.3 |
| Serialization | yaml-cpp |
| UI | Dear ImGui + ImGuizmo (vendored) |
| Logging | spdlog / fmt |
- The Book of Shaders
- Learn OpenGL
- Hazel Engine (inspiration)
