A C++20 pixel-art real-time renderer. Pick OpenGL 3.3 or Vulkan as the graphics backend, GLFW or SDL3 for windowing and input, or compile headless Vulkan for offscreen / CI rendering with no display server. ImGui is bundled and works on every combination — including diegetic UI rendered into an off-screen texture inside the game world.
Define some textures and dynamic transforms, give it an update callback, and you have a running game/application.
Nothofagus::ColorPallete pallete{
{0.0, 0.0, 0.0, 0.0},
{0.0, 0.4, 0.0, 1.0},
{0.2, 0.8, 0.2, 1.0},
{0.5, 1.0, 0.5, 1.0},
};
Nothofagus::IndirectTexture texture({8, 8}, {0.5, 0.5, 0.5, 1.0});
texture.setPallete(pallete)
.setPixels(
{
2,1,3,0,0,3,2,1,
2,1,1,0,0,0,2,1,
2,1,1,1,0,0,2,1,
2,1,2,1,1,0,2,1,
2,1,0,2,1,1,2,1,
2,1,0,0,2,1,2,1,
2,1,0,0,0,2,2,1,
2,1,3,0,0,3,2,1,
}
);
Nothofagus::TextureId textureId = canvas.addTexture(texture);
Nothofagus::Transform transform({75.0f, 75.0f});
Nothofagus::Bellota bellota(transform, textureId);
Nothofagus::BellotaId bellotaId = canvas.addBellota(bellota);
canvas.run(update);A Bellota ("acorn") is a drawable element — a Transform (position, scale,
rotation in degrees) bound to a TextureId, with optional extras like
depthOffset for Z-ordering, a custom MeshId, opacity, tint, or a
multi-layer frame index. Each number in the texture above is an index into the
ColorPallete — yes, it is an indirect color scheme.
Animate by providing an update callback:
float time = 0.0f;
bool rotate = true;
auto update = [&](float dt)
{
time += dt;
ImGui::Begin("Hello there!");
ImGui::Text("May ImGui be with you...");
ImGui::Checkbox("Rotate?", &rotate);
if (rotate)
{
Nothofagus::Bellota& bellota = canvas.bellota(bellotaId);
bellota.transform().angle() = 0.1f * time;
}
ImGui::End();
};
canvas.run(update);Make it interactive with a Controller:
Nothofagus::Controller controller;
controller.registerAction({Nothofagus::Key::W, Nothofagus::DiscreteTrigger::Press}, [&]()
{
canvas.bellota(bellotaId).transform().location().y += 10.0f;
});
canvas.run(update, controller);This is a screenshot of examples/hello_nothofagus.cpp
Use this repository as a git submodule:
git submodule add https://github.com/dantros/nothofagus.git third_party/nothofagus
Then add_subdirectory from your project's CMake file. Flip the backend
options to match your target:
option(NOTHOFAGUS_INSTALL "Disabling installation of Nothofagus" OFF)
# Optional — defaults are GLFW + OpenGL 3.3.
set(NOTHOFAGUS_WINDOW_BACKEND "GLFW" CACHE STRING "") # or "SDL3"
set(NOTHOFAGUS_BACKEND_VULKAN OFF CACHE BOOL "") # ON for Vulkan
set(NOTHOFAGUS_HEADLESS_VULKAN OFF CACHE BOOL "") # ON for offscreen-only
add_subdirectory("third_party/nothofagus")
add_executable(nothofagus_demo
"source/nothofagus_demo.cpp"
)
set_property(TARGET nothofagus_demo PROPERTY CXX_STANDARD 20)
target_include_directories(nothofagus_demo PRIVATE ${NOTHOFAGUS_INCLUDE})
target_link_libraries(nothofagus_demo PRIVATE nothofagus)See the companion repo nothofagus_demo for a complete consumer project.
git clone https://github.com/dantros/nothofagus.git
cd nothofagus
cmake --preset linux-release-glfw-opengl-examples
cmake --build build/linux-release-glfw-opengl-examples --parallel
cmake --install build/linux-release-glfw-opengl-examples
Replace the preset with whatever combination you need — see the
build matrix below. On Windows, swap the prefix for
windows- and you're done.
Third-party dependencies are vendored via git subtree into
third_party/, so a plain git clone is enough. See
third_party/SOURCES.md for the per-dependency
update commands.
The static library and example binaries land in
install/<preset-name>/.
For non-paletted sprites — photographs, screenshots, decoded image files —
use DirectTexture and pass raw RGBA bytes:
Nothofagus::DirectTexture tex({width, height});
tex.setPixels(/* width * height * 4 bytes of RGBA */);
Nothofagus::TextureId texId = canvas.addTexture(tex);
canvas.addBellota({{{x, y}}, texId});→ examples/hello_direct_texture.cpp
makeTextTexture builds in-game text from the bundled font8x8 bitmap font as a
tile-map IndirectTexture (glyphs as layers, the string as the cell grid), so
the whole string draws as one bellota / one draw call through the regular bellota
pipeline — distinct from ImGui text, which is for tool UI. '\n' makes multi-line
text; setText re-spells in place (cheap, map-only re-upload). Multiple font
variants are available via FontType (Basic, ExtLatin, Greek, Box,
Block, Hiragana, ...). writeChar remains the per-glyph primitive for
independently transformable character sprites.
Nothofagus::IndirectTexture banner = Nothofagus::makeTextTexture(
"- Nothofagus -", Nothofagus::FontType::Basic, {1,1,1,1}, {0,0,0,0.8});
canvas.addBellota({{{x, y}}, canvas.addTexture(banner)});
// Single hiragana glyph 0xD into an 8x8 texture (per-glyph primitive):
Nothofagus::IndirectTexture glyph({8, 8}, {0.5, 0.5, 0.5, 1.0});
glyph.setPallete({{0,0,0,0}, {0,0,0,1}});
Nothofagus::writeChar(glyph, 0xD, 0, 0, Nothofagus::FontType::Hiragana);The same Controller handles all three input devices. Mouse positions are
delivered in game canvas coordinates (origin bottom-left), and gamepads are
polled per frame with deadzone + axis normalisation applied for you.
Nothofagus::Controller controller;
controller.registerAction({Key::W, DiscreteTrigger::Press}, [&]{ /* ... */ });
controller.registerMouseAction({MouseButton::Left, DiscreteTrigger::Press}, [&]{ /* ... */ });
controller.registerMouseMove([&](glm::vec2 pos){ /* ... */ });
controller.registerGamepadAction({0, GamepadButton::A, DiscreteTrigger::Press}, [&]{ /* ... */ });
controller.registerGamepadAxis(0, GamepadAxis::LeftX, [&](float v){ /* ... */ });
canvas.run(update, controller);→ examples/test_keyboard.cpp, examples/test_gamepad.cpp
Multi-layer IndirectTexture stores frames as layers.
AnimationStateMachine drives bellota.currentLayer() automatically:
AnimationState idle({0, 1, 2}, {100.0f, 100.0f, 100.0f}, "idle");
AnimationState run ({3, 4}, { 80.0f, 80.0f}, "run");
AnimationStateMachine machine(canvas.bellota(id));
machine.addState("idle", &idle).addState("run", &run);
machine.newAnimationTransition("idle", "start_running", "run");
machine.setState("idle");
// per frame:
machine.update(dt);
machine.transition("start_running");→ markdown_docs/sprite_animations.md, examples/hello_animation.cpp, examples/hello_animation_state_machine.cpp
For tile-based worlds, IndirectTexture::setMap opts a texture into tile-map
mode where each cell selects a layer to draw from a shared atlas. For huge or
sparse worlds, Tilemap / Sparsemap paired with an Explorer keep a pool
of small chunk textures sized to the viewport and rotate world chunks through
them as the camera scrolls — memory stays bounded regardless of world size.
auto tilemapId = canvas.addTilemap(Nothofagus::Tilemap(mapSize, chunkSize, tileSize, palette, tileGraphics));
auto explorerId = canvas.addTilemapExplorer(Nothofagus::TilemapExplorer(tilemapId));
canvas.tilemap(tilemapId).setCell({worldX, worldY}, layerIndex);
canvas.tilemapExplorer(explorerId).setCamera({scrollX, scrollY});→ examples/hello_tilemap.cpp, examples/hello_tilemap_huge.cpp, examples/hello_sparsemap.cpp
Bellotas draw a centered quad sized to their texture by default. Register a custom triangle mesh and attach it to a bellota to draw arbitrary geometry, still sampling the texture through the same shader:
Nothofagus::Mesh mesh;
mesh.vertices = { /* position + uv */ };
mesh.indices = { /* triangles */ };
auto meshId = canvas.addMesh(mesh);
canvas.addBellota({{{x, y}}, texId, meshId});Render bellotas into an off-screen texture and sample it from another bellota — the foundation for diegetic UI, mirrors, mini-maps, and post-processing. Render targets can also feed each other.
auto renderTargetId = canvas.addRenderTarget({64, 64});
auto displayId = canvas.addBellota({{{x, y}}, canvas.renderTargetTexture(renderTargetId)});
canvas.run([&](float dt) {
canvas.renderTo(renderTargetId, {sourceBellotaA, sourceBellotaB});
});→ examples/hello_render_to_texture.cpp, examples/hello_nested_render_targets.cpp
Run ImGui calls into a render target with renderImguiTo to make an
interactive ImGui panel that lives inside the game world. Bake custom TTF
fonts at logical pixel sizes for crisp glyphs.
auto fontId = canvas.bakeImguiFont(canvas.defaultImguiFontSourceId(), 12.0f);
canvas.renderImguiTo(renderTargetId, fontId, [&] {
ImGui::Begin("in-world panel");
ImGui::SliderFloat("value", &v, 0.0f, 1.0f);
ImGui::End();
});→ examples/hello_imgui_rtt.cpp, examples/hello_custom_font.cpp
MarkdownRenderer (built atop imgui_md + md4c) renders a CommonMark
document into the current ImGui window — headings, paragraphs, bold,
italic, inline code, fenced code blocks, lists, tables, blockquotes,
strikethrough, horizontal rules, and clickable links. Style is driven by
the same ImguiFontIds you bake with bakeImguiFont, so heading sizes and
code-font choice are fully under your control.
Nothofagus::MarkdownStyle style;
style.regular = canvas.bakeImguiFont(canvas.defaultImguiFontSourceId(), 16.0f);
style.code = canvas.bakeImguiFont(canvas.defaultImguiFontSourceId(), 14.0f);
style.headings[0] = canvas.bakeImguiFont(canvas.defaultImguiFontSourceId(), 28.0f);
style.headings[1] = canvas.bakeImguiFont(canvas.defaultImguiFontSourceId(), 22.0f);
Nothofagus::MarkdownRenderer markdown(canvas);
markdown.setStyle(style);
markdown.setOpenUrlCallback([](std::string_view url){ /* open in browser */ });
canvas.run([&](float) {
ImGui::Begin("docs");
markdown.print("# Hello\n\nSome **bold** text and a [link](https://...).\n");
ImGui::End();
});imgui-filebrowser is bundled and ready to use — pull in the header and
drive ImGui::FileBrowser directly from any ImGui callback (main canvas
or a render target). Handy for asset pickers, save-game dialogs, or
runtime resource swapping. No Nothofagus wrapper is needed; the addon
ships as part of the engine's third-party tree.
#include <imfilebrowser.h>
ImGui::FileBrowser fileDialog;
fileDialog.SetTitle("Pick a font");
fileDialog.SetTypeFilters({".ttf", ".otf"});
canvas.run([&](float) {
if (ImGui::Button("Open...")) fileDialog.Open();
fileDialog.Display();
if (fileDialog.HasSelected()) {
auto path = fileDialog.GetSelected();
fileDialog.ClearSelected();
// ... load the file ...
}
});→ examples/hello_custom_font.cpp uses this pattern to let you pick a TTF at runtime.
takeScreenshot() returns the last rendered frame as a DirectTexture — you
can save it via your image library of choice, or load it straight back into
the canvas as a regular texture.
Nothofagus::DirectTexture shot = canvas.takeScreenshot();
Nothofagus::TextureData data = shot.generateTextureData(); // raw RGBA, top-to-bottom→ examples/hello_screenshot.cpp
Two ways to run without a visible window:
headless=trueat construction — the window still exists (GLFW/SDL) but is hidden. Requires a display server on Linux. Good for local automation.-DNOTHOFAGUS_HEADLESS_VULKAN=ONat build time — no window and no display server, pure offscreen Vulkan rendering. Designed for CI lanes using software Vulkan (SwiftShader / lavapipe).
Either way, drive the loop manually with tick(dt):
Nothofagus::Canvas canvas({15, 10}, "test", {0,0,0}, 1, 14, /*headless=*/true);
for (int i = 0; i < 10; ++i) canvas.tick(16.0f);
Nothofagus::DirectTexture shot = canvas.takeScreenshot();Presets follow the pattern {platform}-{buildtype}-{window}-{graphics}[-examples].
Add -examples to also build the demo binaries.
| Platform | Window backend | Graphics backend |
|---|---|---|
windows, linux |
glfw |
opengl or vulkan |
windows, linux |
sdl3 |
opengl or vulkan |
windows, linux |
headless |
vulkan (offscreen, no display server) |
{buildtype} is debug or release. See CMakePresets.json
for the full preset list and CLAUDE.md for per-option details
(NOTHOFAGUS_WINDOW_BACKEND, NOTHOFAGUS_BACKEND_VULKAN,
NOTHOFAGUS_HEADLESS_VULKAN, NOTHOFAGUS_BUILD_EXAMPLES, ...).
| File | Demonstrates |
|---|---|
| hello_nothofagus.cpp | Basic setup — IndirectTexture + ImGui + rotation |
| hello_direct_texture.cpp | DirectTexture (raw RGBA) |
| hello_animation.cpp | Single-state frame animation |
| hello_animation_state_machine.cpp | Full state machine with WASD-driven transitions |
| hello_layers.cpp | Depth-based layering |
| hello_tint.cpp | Per-bellota color tinting |
| hello_text.cpp | Text rendering with the bundled bitmap font |
| hello_markdown.cpp | Markdown rendering via imgui_md |
| hello_mesh.cpp | Custom triangle meshes |
| hello_tilemap.cpp | Tile-map mode of IndirectTexture |
| hello_tilemap_huge.cpp | Huge tilemaps via Tilemap + TilemapExplorer pool |
| hello_sparsemap.cpp | Sparse / unbounded tilemaps with simulated streaming |
| hello_render_to_texture.cpp | Off-screen render targets sampled by bellotas |
| hello_nested_render_targets.cpp | One RTT's output feeding another |
| hello_imgui_rtt.cpp | Diegetic ImGui — UI rendered inside the game world |
| hello_custom_font.cpp | User-supplied TTF fonts via addImguiFontSource |
| hello_screenshot.cpp | takeScreenshot() — capture frame as a texture |
| hello_headless.cpp | Headless mode + manual tick() |
| test_keyboard.cpp | Keyboard input handling |
| test_gamepad.cpp | Gamepad sticks, D-pad, buttons, ImGui status |
| test_create_destroy.cpp | Object lifecycle |
- CLAUDE.md — full architecture and API patterns reference: backend abstractions, Vulkan presentation policy, public API patterns, lifecycle rules, and complete CMake option list.
- markdown_docs/sprite_animations.md — sprite animation deep dive.
- CMakePresets.json — every supported build configuration.
Doxygen documentation is opt-in via the NOTHOFAGUS_BUILD_DOCS=ON CMake
option (requires Doxygen
installed). Generate by hand from the repo root:
doxygen DoxyfileOr build them as part of an install:
cmake --preset linux-release-glfw-opengl-examples -DNOTHOFAGUS_BUILD_DOCS=ON
cmake --build build/linux-release-glfw-opengl-examples --parallel
cmake --build build/linux-release-glfw-opengl-examples --target doc_doxygenYou'll need CMake, Ninja, and a C++20 compiler (clang-cl on Windows, clang++ on Linux are what the presets use; Visual Studio Community works too). For the Vulkan backend, install the Vulkan SDK.
Third-party code is vendored via git subtree into
third_party/. Current set: GLFW, SDL3, glad (OpenGL loader), glm (math),
Dear ImGui (v1.92.8), spdlog, font8x8 (bitmap font), imgui_md + md4c
(markdown rendering), imgui-filebrowser, vk-bootstrap,
VulkanMemoryAllocator, Catch2 (tests). See
third_party/SOURCES.md for upstream URLs,
pinned versions, and the git subtree pull command to update each one.
MIT © Daniel Calderón
