GameEngine
My first engine: an OpenGL renderer with a skeletal animation importer, a templated ECS and an ImGui editor, later ported to Vulkan.
- Period
- Nov 2021 – Apr 2022
- Role
- Solo
- Status
- archived
- commits
- 125
- own source
- 6,091 LOC
- Vulkan layer
- 2,113 LOC
Highlights
- Skeletal animation from glTF, written by hand: inverse bind matrices read straight from the accessor, joint parents reconstructed by walking node children, then animation channels and samplers decoded into per-joint translation, rotation and scale tracks keyed by time.
- A templated ECS where components live in a static ComponentContainer<T> and entities hold indices rather than pointers, with variadic CreateComponent<T>(args...) and a move-constructor specialization so components survive their vector being resized.
- An ImGui editor with a docked viewport, scene hierarchy, properties panel, console and file browser, about 1,000 lines of it.
- Single-file GLSL split per stage and compiled with shaderc, with a shader cache and reflection-driven material shaders.
- Ported from OpenGL to Vulkan on its own branch: instance, device, swapchain, render pass, pipeline, descriptor pool and buffer abstractions, about 2,100 lines.
The link points at branch Vulkan, which is the final state. master stops in February 2022, before the port.
The animation system was lost, not finished: the February 2022 importer rewrite carried over everything except skinning, and only the Joint and Animation structs survive at the tip. It works in the December 2021 history.
The problem
My first engine, and the one where the hardest thing in it worked: skeletal animation imported by hand straight from glTF, with no animation library underneath it, playing on screen. Around it are a templated ECS, a shader pipeline with caching, an ImGui editor with a docked viewport and scene hierarchy, and a full port of the renderer from OpenGL to Vulkan. Everything here is mine and everything was built without following instructions, which was the reason for starting it.
Before this I had built a renderer by following a tutorial series. That project is lost and is not on GitHub.
This engine was built deliberately away from that tutorial. It is object-oriented and GameObject-based where the one before it was an ECS, it uses its own templated component storage rather than entt, and it imports glTF by hand rather than through a library.
Skeletal animation
The animation importer was written by hand straight from glTF, with no animation library underneath it. Inverse bind matrices are read directly out of the accessor buffer. Joint parents are reconstructed by walking each node's children and mapping node indices to joint indices, because glTF stores the hierarchy as children rather than parents and the skinning maths wants it the other way around. Animation channels are then resolved to their samplers, and keyframe times and the translation, rotation and scale tracks are decoded per joint.
It ends up stored as keyframe time to joint to matrix, with each joint holding a parent index, an inverse bind matrix and a pose matrix. Skeleton nodes are kept out of the scene tree entirely when a model has no animation, so static models do not pay for a feature they do not use.
Animations played on screen.
It was then lost, not finished. The February 2022 rewrite of the importer moved to a
new asset format and
carried over everything except the skinning, and the commit message says so. Only the Joint and Animation structs survive at the tip of the
branch. The working version is still in the December 2021 history, which is where any
demo of it has to come from.
The ECS
Components live in a static ComponentContainer<T> per type, and
entities hold indices into those containers rather than pointers. CreateComponent<T>(args...) is variadic, so adding a new component
type needs no new engine code at all.
The interesting problem with an index-based design is what happens when a component vector resizes and moves everything, which is exactly the case that breaks naive implementations of this. A move-constructor specialization handles it, so components survive the reallocation with their indices still meaning what they meant before.
This is the first of three component systems I wrote across these engines, and its limits are what drove the next two. The storage is a static data member per component type, reached through template machinery, which means there is exactly one world and there can only ever be one. Multi-world support, which you want the moment you have an editor scene and a running scene, has nowhere to go. Neither does multi-threading, because static storage shared across every system is the one thing you cannot hand to two threads. The next engine moved to entt, and the one after that built a scheduler on top of entt that could reason about which systems touched which components. Both of those are answers to a question this design made me ask.
Editor and tooling
The editor is roughly 1,000 lines of ImGui: a docked viewport, a scene hierarchy, a properties panel, a console wired into the engine's own logging, and a file browser. Alongside it there is scene and sub-scene loading, a project structure, UUIDs and an asset manager.
Shaders are written as a single GLSL file and split per stage before being compiled with shaderc, with caching so unchanged shaders do not recompile. That approach survived into both engines that followed this one, and it is the piece of this project that turned out to be most reusable.
The Vulkan port
In February 2022 I started porting the renderer to Vulkan on its own branch, from an engine that already worked in OpenGL. It came to about 2,100 lines: instance, device, swapchain, render pass, pipeline, descriptor pool and buffer abstractions, plus semaphore and fence synchronization and window-resize handling.