VoidEngine is a desktop-oriented C++20 game engine with an integrated editor, OpenGL renderer, ECS-based scene layer, physics, audio, Lua scripting, runtime game packages, and asset pipeline tooling.
The goal of this project is to explore how a practical game engine is structured end to end: reusable engine code, editor workflows, runtime loading, tools, tests, and a runnable sample project in one CMake-based repository.
- Integrated
VoidEditorwith scene hierarchy, inspector, viewport, asset browser, console, profiler, undo history, gizmos, and play mode. - ECS scene system with UUID-backed entities, transform hierarchy, serialization, prefabs, scripting hooks, and runtime package loading.
- OpenGL rendering layer with shaders, meshes, textures, framebuffers, forward/deferred rendering paths, shadows, skybox, IBL, SSAO, bloom, and post-processing.
- Gameplay-oriented systems for physics, raycasting, triggers, input mapping, gamepad support, audio playback, music streaming, Lua scripting, particles, and terrain.
- Asset pipeline tools for baking meshes/textures and packaging runtime assets into
.vepackarchives. - Standalone subsystem tests covering rendering, scenes, physics, audio, scripting, editor panels, resources, and asset tooling.
| Area | Technologies |
|---|---|
| Language | C++20 |
| Build and packaging | CMake, CPack |
| Rendering | OpenGL, GLSL, GLFW, GLAD, GLM |
| Editor UI | Dear ImGui, ImGuizmo, tinyfiledialogs |
| Assets and data | Assimp, stb, nlohmann/json, custom .vemesh, .vetex, .vepack, .vegame.json formats |
| Physics | Jolt Physics |
| Audio | OpenAL Soft |
| Scripting | Lua, sol2 |
| Tests | Standalone CMake test executables |
- Documentation index
- Editor Guide: editor layout, panels, scene workflow, play mode, shortcuts, asset drag-drop, and extension points.
- Engine API Guide: public C++ APIs, application lifecycle, scene/ECS, rendering, physics, scripting, input, audio, and reusable editor panels.
- Assets, Packages, and Tooling: scene files, game packages, controls configs,
vebake,vepack, and runtime asset workflows. - Build, Testing, and Packaging: CMake targets, dependencies, tests, verification flows, install, and CPack packaging.
VoidEngine: the reusable static engine library inengine/VoidEditor: an editor application for inspecting scenes and game packagesVoidDemo: a runtime executable that boots the included sample packagevebake: offline asset baking tool for meshes and texturesvepack: archive tool for.vepackruntime asset bundlesgame/signal_breach.vegame.json: a ready-to-run sample package used to test the runtime/editor workflowtests/: standalone test executables that cover engine subsystems
VoidEngine is designed for desktop OpenGL projects where you want one repository to contain:
- the core application/runtime layer
- the renderer
- scene and ECS management
- editor-facing panels and play mode
- asset import/bake/package tooling
- runtime package loading
- automated tests for core subsystems
It is a practical engine codebase, not only a toy sample. The current implementation is strongest in the following areas.
| System | What is implemented | What it is for |
|---|---|---|
| Application core | ve::Application, Window, Time, EventBus, Input, GamepadManager, JobSystem, SplashScreen |
Bootstrapping a desktop app, owning subsystems, running the main loop, and wiring per-frame update/render flow |
| ECS / scene graph | Scene, Entity, generic component stores, UUID-backed entity pool, parent-child transform hierarchy, snapshots |
Building gameplay objects, editor objects, and serializable runtime scenes |
| Rendering | RenderDevice, ShaderProgram, Mesh, Texture2D, CubemapTexture, Framebuffer, Camera |
OpenGL resource management, shader compilation/hot reload, drawing, camera control, off-screen rendering |
| Materials | Material, MaterialInstance, texture bindings, custom uniform properties |
Authoring Blinn-Phong or PBR-style material setups and per-instance overrides |
| Scene rendering | forward and deferred paths, shadow resources, skybox, IBL, SSAO, bloom, tone mapping, color grading, FXAA, vignette | Building lit 3D scenes with post-processing and environment lighting |
| Visibility and batching | instancing, static batching, frustum culling, occlusion culling, Hi-Z infrastructure, culling stats | Reducing draw overhead and exposing render visibility diagnostics |
| Physics | PhysicsWorld, fixed physics tick, rigid bodies, colliders, triggers, raycasts, screen picking |
Deterministic gameplay stepping, movement, collision, overlap detection, and editor/runtime picking |
| Audio | AudioDevice, AudioClip, AudioSource, AudioMixer, MusicStream, MusicPlaylist |
SFX and music playback with groups, priorities, virtualization, and playlist/crossfade support |
| Scripting | ScriptComponent, Lua runtime integration, hot reload, ECS/scene bindings |
Driving gameplay logic from Lua with update/physics hooks and scene mutation |
| Terrain and particles | terrain heightfield component, splat layers, foliage layers, particle emitters | Outdoor scenes, terrain LOD, billboard/mesh particles, lightweight VFX |
| Asset pipeline | AssetBaker, AssetPack, TextureManager, ResourceManager |
Converting source files into runtime-friendly formats and managing loaded resources |
| Serialization | SceneSerializer, GamePackage |
Saving/loading scenes to JSON and grouping startup scene + controls + assets into a runtime package |
| Tool | What is implemented | What it is for |
|---|---|---|
VoidEditor |
hierarchy, inspector, viewport, asset browser, console, profiler, undo history, scene/game tabs, gizmos | Working with scenes and packages visually, then entering play mode from the same application |
ViewportPanel |
Scene tab, Game tab, editor camera, game camera, picking, transform gizmos, snap settings | Editing transforms and verifying runtime camera/gameplay output inside the editor |
| Undo/redo | UndoHistory, value commands, transaction commands, scene commands |
Keeping editor modifications reversible and grouping drag/gizmo edits into clean transactions |
vebake |
mesh and texture baking into .vemesh / .vetex |
Converting source assets into formats that are faster for runtime loading |
vepack |
pack/list/extract .vepack archives |
Shipping runtime asset bundles and inspecting archive contents |
VoidEngine is currently well-suited for:
- single-player desktop 3D games
- gameplay sandboxes built around ECS entities and components
- editor-backed prototypes where play mode and scene editing live in one app
- package-driven demos where a
.vegame.jsondecides startup scene, controls, and asset roots - projects that want engine code, editor code, and runtime assets in one CMake repository
Requirements:
- CMake
3.25+ - C++20 compiler
- Git
- Desktop OS with OpenGL support
- Internet access on the first configure step, because dependencies are fetched through
FetchContent
Configure and build from the repository root:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build --parallelIf you use a multi-config generator such as Visual Studio or Xcode, the executable path may include a configuration directory such as Debug or Release.
./build/bin/VoidEditor./build/bin/VoidEditor game/signal_breach.vegame.json
./build/bin/VoidDemo game/signal_breach.vegame.jsonThe sample package is included to verify that the runtime package, controls config, assets, scripting, and editor/game workflow all work in a real project. It is not the main subject of this README; the engine is.
The main convenience include is:
#include <void_engine/engine.h>That header aggregates the public engine API from:
void_engine/core/*void_engine/platform/*void_engine/renderer/*void_engine/ui/*void_engine/scene/*void_engine/editor/*
If you prefer narrow includes, you can include only the headers you need, such as:
#include <void_engine/core/application.h>
#include <void_engine/scene/scene.h>
#include <void_engine/renderer/render_device.h>
#include <void_engine/renderer/camera.h>The main integration point is ve::Application. You derive from it, override lifecycle methods, and use the subsystems owned by the base class.
#include <void_engine/engine.h>
class MinimalApp final : public ve::Application {
public:
MinimalApp()
: ve::Application(makeConfig()) {}
protected:
void onInit() override {
m_device.init();
m_scene.initPhysics();
m_camera.setPerspective(60.0f, getWindow().getAspectRatio(), 0.1f, 500.0f);
m_camera.setPosition({0.0f, 2.0f, 6.0f});
auto sun = m_scene.createEntity();
m_scene.addTransform(sun);
m_scene.addLight(sun, ve::LightComponent::makeDirectional({-0.4f, -1.0f, -0.2f}));
auto actor = m_scene.createEntity();
m_scene.addName(actor, "Actor");
auto& transform = m_scene.addTransform(actor);
transform.position = {0.0f, 0.5f, 0.0f};
m_scene.addRigidbody(actor, {.motionType = ve::PhysicsMotionType::Dynamic});
m_scene.addCollider(actor, ve::ColliderComponent::makeBox({0.5f, 0.5f, 0.5f}));
}
void onWindowResize(const ve::WindowResizeEvent&) override {
m_camera.setAspectRatio(getWindow().getAspectRatio());
}
void onPhysicsUpdate(ve::f64 fixedDt) override {
m_scene.stepPhysics(static_cast<ve::f32>(fixedDt));
m_scene.stepScriptPhysics(static_cast<ve::f32>(fixedDt), &getInput());
}
void onUpdate(ve::f64 dt) override {
m_camera.update(dt, getInput());
m_scene.stepAnimations(static_cast<ve::f32>(dt));
m_scene.stepParticles(static_cast<ve::f32>(dt));
m_scene.stepScripts(static_cast<ve::f32>(dt), &getInput());
}
void onRender() override {
m_device.setViewport(0, 0, getWindow().getWidth(), getWindow().getHeight());
m_device.setClearColor(0.08f, 0.09f, 0.11f, 1.0f);
m_device.clear();
m_scene.render(m_camera, m_device);
}
private:
static ve::AppConfig makeConfig() {
ve::AppConfig cfg;
cfg.window.title = "VoidEngine Minimal App";
cfg.window.width = 1600;
cfg.window.height = 900;
cfg.enableAudio = true;
return cfg;
}
ve::Scene m_scene;
ve::RenderDevice m_device;
ve::FreeCamera m_camera;
};
int main() {
MinimalApp app;
app.run();
}What this gives you:
- window creation and OpenGL context setup
- a fixed physics tick owned by the application
- input, audio, jobs, time, and event systems
- a place to update gameplay (
onUpdate) - a place to run deterministic simulation (
onPhysicsUpdate) - a place to render (
onRender)
ve::Scene is the central runtime/editor container. It owns entities, generic component stores, physics integration, render integration, and serialization entry points.
Implemented convenience components include:
TransformComponentMeshRendererComponentRigidbodyComponentColliderComponentLightComponentTriggerComponentScriptComponentNameComponentAnimatorComponentTerrainComponentParticleEmitterComponent
ve::Scene scene;
ve::Entity player = scene.createEntity();
scene.addName(player, "Player");
auto& transform = scene.addTransform(player);
transform.position = {0.0f, 1.0f, 0.0f};
transform.setEulerDegrees({0.0f, 45.0f, 0.0f});
scene.addRigidbody(player, {
.motionType = ve::PhysicsMotionType::Dynamic,
.mass = 80.0f,
.linearDamping = 0.15f
});
scene.addCollider(player, ve::ColliderComponent::makeCapsule(0.9f, 0.35f));
ve::Entity cameraPivot = scene.createEntity();
scene.addTransform(cameraPivot);
scene.setParent(cameraPivot, player);scene.each<ve::TransformComponent, ve::RigidbodyComponent>(
[](ve::Entity entity, ve::TransformComponent& transform, ve::RigidbodyComponent& body) {
if (body.isDynamic()) {
transform.position.y += 0.01f;
}
}
);You can render scene-driven entities through Scene::render, while resource types such as Mesh, ShaderProgram, Texture2D, and Material remain usable directly.
ve::ShaderProgram shader;
shader.loadFromFile("content/shaders/pbr_lit.glsl");
ve::Mesh crateMesh;
crateMesh.loadBaked("content/meshes/crate.vemesh");
ve::Texture2D crateAlbedo;
crateAlbedo.loadBakedFromFile("content/textures/crate_albedo.vetex");
ve::Material crateMaterial = ve::Material::createTextured(&shader, &crateAlbedo);
crateMaterial.setProperty("u_tint", glm::vec3(1.0f, 1.0f, 1.0f));
ve::Entity crate = scene.createEntity();
scene.addName(crate, "Crate");
scene.addTransform(crate).position = {0.0f, 0.5f, 0.0f};
scene.addMeshRenderer(crate, {
.mesh = &crateMesh,
.material = &crateMaterial,
.enableInstancing = true
});- GLSL shaders from separate files or combined
#type vertex/#type fragmentfiles - shader hot reload
- forward and deferred rendering paths
- skybox and image-based lighting resources
- directional, point, and spot lights
- shadow-map resources and cascade data plumbing
- terrain rendering with splat layers and foliage layers
- billboard or mesh-based particle emitters
- instancing and static batching flags on mesh renderer components
- culling and renderer statistics exposed through scene stats
The engine also exposes a generic handle-based ResourceManager and a higher-level TextureManager.
Use this when you want:
- reference-counted handles instead of raw ownership everywhere
- loading state tracking (
Loading,Ready,Failed) - keyed lookup for reusable resources
- placeholder/fallback textures for missing assets
ve::TextureManager textures;
auto albedoHandle = textures.load(
"crate_albedo",
"content/textures/crate_albedo.png",
{.generateMipmaps = true},
ve::TexturePlaceholder::White
);
const ve::Texture2D* albedo = textures.resolve(
albedoHandle,
ve::TexturePlaceholder::White
);This is useful in editor/runtime code where you need safe fallback behavior instead of failing hard on every missing texture.
ve::Input is the low-level polling layer. ve::InputMap lets you define higher-level actions such as Jump, Fire, or MoveForward, then bind them to keyboard, mouse, or gamepad input and serialize that mapping to JSON.
#include <GLFW/glfw3.h>
ve::InputMap inputMap;
inputMap.addAction("move_forward");
inputMap.addAction("fire");
inputMap.addAction("dash");
inputMap.addBinding("move_forward", {ve::InputSource::Keyboard, GLFW_KEY_W});
inputMap.addBinding("move_forward", {ve::InputSource::GamepadAxis, GLFW_GAMEPAD_AXIS_LEFT_Y, -1.0f, 0.20f});
inputMap.addBinding("fire", {ve::InputSource::MouseButton, GLFW_MOUSE_BUTTON_LEFT});
inputMap.addBinding("dash", {ve::InputSource::Keyboard, GLFW_KEY_SPACE});
// each frame
inputMap.update(getInput());
if (inputMap.isActionPressed("dash")) {
// trigger dash once
}
float moveForward = inputMap.getActionValue("move_forward");
bool firing = inputMap.isActionHeld("fire");
inputMap.saveToFile("config/controls.json");Use this layer if you want rebinding, gamepad support, and data-driven action names instead of hard-coded key checks scattered throughout gameplay code.
The application base class already owns an AudioDevice. On top of that you can create clips, sources, a mixer, music streams, and playlists.
ve::AudioMixer mixer;
ve::AudioClip laserClip;
laserClip.loadFromFile(getAudioDevice(), "content/audio/laser.wav");
ve::AudioSource laser;
laser.init(getAudioDevice());
laser.setClip(&laserClip);
laser.setMixer(&mixer);
laser.setMixerGroup(ve::AudioMixerGroup::Sfx);
laser.setVolume(0.8f);
laser.setPriority(5);
// when the player fires
laser.play();Current audio stack includes:
- clip loading from file or PCM memory
- source playback, pause, stop, looping, pitch, 3D position, velocity
- mixer groups and master routing
- source priority and preemption
- music streaming and playlists
- virtual backend support used by tests for deterministic machines without a real device
Scene integrates physics with ECS components and exposes both low-level PhysicsWorld access and scene-aware helpers.
ve::SceneRaycastHit hit;
ve::PhysicsRay ray;
ray.origin = {0.0f, 2.0f, 6.0f};
ray.direction = glm::normalize(glm::vec3(0.0f, -0.2f, -1.0f));
ray.maxDistance = 100.0f;
if (scene.raycastClosest(ray, hit)) {
if (scene.hasName(hit.entity)) {
std::cout << "Hit entity: " << scene.getName(hit.entity).name << "\n";
}
}Editor-style screen picking is also available through:
Scene::screenPointToRayScene::pickClosestScene::pickAll
Trigger overlap queries are available through:
Scene::getTriggerOverlapsScene::isTriggerOverlapping
Attach a script path to an entity through ScriptComponent. The runtime will load the Lua file, bind ECS/scene functionality, and call lifecycle hooks.
Implemented hook names used by the current runtime/tests:
on_init()on_update(dt)on_physics_update(dt)on_reload()on_shutdown()
Runtime characteristics:
- scripts can mutate transforms, names, and other entity state
- scripts can create and destroy ECS entities through scene bindings
- hot reload is supported through
ScriptComponent::hotReload - physics and input bindings are available from Lua runtime code
Attach a script like this:
ve::Entity scripted = scene.createEntity();
scene.addTransform(scripted);
scene.addScript(scripted, {
.path = "content/scripts/player_controller.lua",
.enabled = true,
.hotReload = true
});VoidEngine has two different but related persistence layers:
SceneSerializer: serializes a scene to JSONGamePackage: groups startup scene, controls config, and asset paths into a.vegame.json
Important implementation detail:
MeshRendererComponentstores non-owningMesh*andIMaterial*- because of that,
SceneSerializerusesAssetResolvercallbacks to convert pointers to string IDs on save and string IDs back to pointers on load - if you do not provide a resolver, scenes still load, but mesh/material references are not reconstructed automatically
ve::SceneSerializer serializer(scene);
serializer.saveToFile("content/scenes/my_scene.scene.json");
serializer.loadFromFile("content/scenes/my_scene.scene.json");ve::GamePackage pkg;
pkg.title = "My VoidEngine Project";
pkg.description = "Runtime package for a desktop prototype";
pkg.scenarioId = "my_project";
pkg.startupScene = "content/scenes/my_scene.scene.json";
pkg.controlsConfig = "config/controls.json";
pkg.assetRoot = "content";
pkg.saveToFile("my_project.vegame.json");This package format is what the editor and runtime use to open a complete playable setup.
The asset pipeline is split into two concerns:
- baking source files into runtime-friendly formats
- packing directories into a runtime archive
vebake converts source assets into engine-specific binary formats:
.vemesh: baked mesh, deduplicated vertices, precomputed bounds.vetex: baked texture, offline mip chain, optional lossless compression
Usage:
./build/bin/vebake mesh input.obj output.vemesh
./build/bin/vebake texture input.png output.vetex --srgbvepack produces .vepack archives or inspects/extracts them:
./build/bin/vepack assets/ build/install/assets.vepack
./build/bin/vepack --list build/install/assets.vepack
./build/bin/vepack --extract build/install/assets.vepack extracted-assetsThese tools are useful when you want runtime loading to avoid repeated expensive parsing of source formats.
VoidEditor is the visual front-end for the engine. The current editor stack includes:
- scene hierarchy panel
- inspector panel
- asset browser panel
- console panel
- profiler panel
- undo history panel
- viewport with
SceneandGametabs - transform gizmos with translate/rotate/scale modes
- snapping for translate/rotate/scale
- play mode integration
- Start the editor with no arguments for a clean scene workflow.
- Start the editor with a
.vegame.jsonif you want to boot a packaged project immediately. - Use the
Scenetab to inspect and edit through the editor camera. - Use the
Gametab to inspect the runtime/game camera. - Press
Ctrl+Pto toggle play mode.
The editor code is not a separate toy UI; it is directly wired into engine-side scene, viewport, undo, and runtime package systems.
The project uses standalone executables rather than a single ctest suite. That keeps subsystem tests directly runnable.
Example:
cmake --build build --target TestGamePackage
./build/bin/TestGamePackageThe repository currently includes tests for:
- scenes and generic component APIs
- rendering and framebuffers
- shaders and textures
- physics, triggers, and raycasting
- audio device, mixer, playback, and music streaming
- Lua scripting
- resource and asset pipeline systems
- editor panels and play mode
Install into a packaged layout:
cmake --build build --target installCreate distributable packages with CPack:
cd build
cpackCurrent packaging setup:
- macOS:
.appbundle and DMG - Windows: NSIS and ZIP
- Linux:
TGZandDEB
engine/: reusable engine libraryeditor/: editor applicationdemo/: sample runtime application and package bootstrap logicgame/: included sample package and configassets/: runtime assetssamples/: sample scenes, scripts, and configstools/:vebakeandvepacktests/: standalone subsystem testscmake/: dependency and packaging setup
VoidEngine already covers a serious amount of ground for a public engine repository:
- engine runtime
- ECS scene layer
- renderer
- editor
- package format
- tools
- tests
At the same time, it is still an actively evolving codebase, not a finished commercial engine. Public documentation should therefore be read as documentation of the current implementation, not a promise that every future API detail will remain unchanged.
This repository currently does not contain a top-level LICENSE file. If you want to publish it publicly for others to use, fork, or evaluate, adding a license is the next concrete step after this README.
