Gabrieal Jimmy 2c99305fb0 init
2026-10-02 12:42:00 -05:00
2026-10-02 12:42:00 -05:00
2026-10-02 12:42:00 -05:00
2026-10-02 12:42:00 -05:00
2026-10-02 12:42:00 -05:00
2026-10-02 12:42:00 -05:00
2026-10-02 12:42:00 -05:00
2026-10-02 12:42:00 -05:00
2026-10-02 12:42:00 -05:00
2026-10-02 12:42:00 -05:00
2026-10-02 12:42:00 -05:00
2026-10-02 12:42:00 -05:00
2026-10-02 12:42:00 -05:00

HD-2D Engine

A small C++20 / OpenGL 4.5 engine that recreates the look of Square Enix's HD-2D style (Octopath Traveler, Triangle Strategy, Live A Live, Dragon Quest III HD-2D): a 3D diorama world where the ground is skinned in pixel art, characters and small props (flowers, grass tufts, tall grass) are billboarded pixel-art sprites, and scenery like trees, bushes and rocks are real 3D models with higher-resolution painted textures. Lighting, shadows, ambient occlusion, fog, god rays, water, fire, rain and particles are all rendered as high-resolution 3D effects.

demo heavy rain

What makes HD-2D look like HD-2D

Trait How this engine does it
3D world built from pixel-art textures Tile-based terraced terrain (Terrain) with pixel-art material layers, organically jittered material borders at texel granularity, grass overhangs on cliffs, baked corner AO
Crisp pixels at any distance "Sharp bilinear" sampling (pixelart.glsl): hard texel edges when magnified, antialiased + mipmapped when minified (no GL_NEAREST shimmer)
Upright 2D sprites in 3D Characters and small props are camera-facing (Y-axis) billboards (BillboardBatch), planted by their lowest opaque pixel row
3D scenery with painted textures Trees, bushes, rocks and the stone bridge are instanced glTF models (ModelLibrary, ModelBatch) built in Blender: leaf-card canopies with alpha-tested 1K leaf textures, rounded custom normals for soft stylized shading, sun translucency, wind sway from per-vertex weights, emissive materials (glowing coals)
Grounded objects Two kinds of ambient occlusion: half-res SSAO (depth-reconstructed normals, depth-aware blur, multiplied into the opaque scene before water) and a baked ground contact occlusion map (GroundOcclusion) that puts a dark core at every trunk / bush / rock base plus a soft canopy shade, applied to terrain, sprites and models near the ground
Nothing hides the player Scenery between the camera and the player is dithered away around the sprite
Sprites cast/receive real shadows Shadow pass renders sprites as sun-facing cards so silhouettes are never edge-on; receivers sample their own sun-facing card to avoid self-shadowing
Miniature / diorama feel Narrow FOV (~30°) high camera + strong depth-of-field with an extra tilt-shift band
Soft, warm, glowing light HDR pipeline, Karis-averaged mip-chain bloom, ACES tonemap, lift/gain grading, vignette, subtle chromatic aberration and grain
Fog and god rays Half-res ray-marched height fog; every step samples the sun shadow map and a sparse "canopy" beam pattern, giving physically shadowed light shafts; depth-aware upsample
Dappled forest light Animated canopy cookie projected along the sun direction onto every surface
Beautiful water Planar reflections, depth-aware refraction, per-channel absorption, analytic-derivative wave normals, shore distance field driving foam + ripples, strong HDR sun glints plus a field of twinkling point sparkles that bloom and depth of field turn into bokeh, caustics on the bottom
Living foliage Instanced swaying grass with wind gusts; tall grass and bushes bend and rustle away from the player (vertex shader, Pokémon-style patches); leaf flutter scales with wind strength
High-res particles Fireflies (also feed point lights) and dust motes that only glow inside sun shafts
Fire Brazier fires (ParticleSystem::addFire): noise-eroded additive flame tongues with a hot-core-to-red color ramp, turbulent embers, lit smoke puffs, a glow halo, and a flickering point light that also scatters into the fog
Weather Weather blends the clear look into a storm preset (weak grey sun, overcast sky, thick low fog, gusty wind): procedural world-space rain streaks (up to 18k, one draw call), ground splashes, raindrop ripple rings on the water, wet darker glossy surfaces, ambient fireflies fade out

References: Unreal Engine's Octopath Traveler spotlight, the Octopath Traveler II interview, the Unreal Fest Europe 2019 talk "The Fusion of Nostalgia and Novelty", and The Verge on HD-2D's evolution.

Building

Requirements: CMake ≥ 3.20, a C++20 compiler, OpenGL 4.5, libepoxy. GLFW, GLM, stb and cgltf are used from the system when found and fetched otherwise; Dear ImGui is always fetched.

# Arch / CachyOS
sudo pacman -S cmake ninja glfw glm libepoxy stb

cmake -S . -B build -G Ninja
ninja -C build
./build/hd2d_demo

The asset path is baked in at configure time (HD2D_ASSET_DIR), so the demo can be run from any working directory.

Demo controls

Input Action
WASD / arrows Move the sprite (camera-relative)
Q / E, right-drag Rotate / orbit camera
Mouse wheel Zoom
F1 Toggle the tuning panels (sun, sky, AO, fog, god rays, water, DoF, bloom, grading, weather)
R Toggle heavy rain (5 s transition)
F5 Hot-reload shaders
F12 Screenshot to screenshots/
Esc Quit

Scriptable captures (useful for look-dev and regression checks):

./build/hd2d_demo --screenshot out.png --frames 60 --pos 25,43.6 \
    --sun -35,38 --yaw 20 --dist 12 --move 1,0 --rain 1 --set godRays=6 --set dof=0

--rain 0..1 starts the capture already raining. --set accepts: volumetrics dof bloom reflections occluderFade ssao groundAO ssaoRadius ssaoIntensity fogDensity fogHeightFalloff godRays shaftStrength shaftCoverage shaftScale cookieStrength exposure bloomStrength tiltShift focusRange.

Layout

engine/include/hd2d/   public headers
engine/src/            engine implementation
assets/shaders/        GLSL (common.glsl = shared UBO + lighting helpers)
assets/textures/       PixelLab-generated pixel art (terrain tiles, grass, flowers)
assets/sprites/        character sprites
assets/models/         scenery.glb (trees, bushes, rocks, bridge) + src/ painted textures
demo/                  meadow demo scene + main loop
tools/pixellab.py      pixel-art generator, driven by tools/assets.json
tools/scenery_textures.py  paints the high-res scenery textures
tools/blender_scenery.py   builds + exports the 3D scenery in Blender

Frame pipeline (Renderer::render)

flowchart LR
  A[Sun shadow map<br/>terrain, 3D models, sun-facing sprite cards] --> B[Planar reflection<br/>half res, clipped]
  B --> C[HDR scene<br/>sky, terrain, models, billboards]
  C --> C2[SSAO<br/>half res, multiplied in]
  C2 --> D[Water<br/>reads color/depth copies]
  D --> E[Volumetric fog + god rays<br/>half res ray march, fire haze]
  E --> F[Fog composite<br/>depth-aware upsample]
  F --> F2[Rain streaks]
  F2 --> G[Particles<br/>fire, smoke, fireflies]
  G --> H[Depth of field<br/>tilt-shift]
  H --> I[Bloom mip chain]
  I --> J[Tonemap + grade]

All per-frame data lives in one std140 uniform block (FrameUBO in Renderer.cpp ⇄ Frame in common.glsl); keep them in sync.

Using the engine in your own project

hd2d::Window window;   window.create(1600, 900, "My Game");
hd2d::Renderer renderer; renderer.init("assets/shaders", window.width(), window.height());

hd2d::Terrain terrain;  /* init, setLevel/setMaterial, textures.load(...), build() */
hd2d::BillboardBatch hero; hero.loadTexture("assets/sprites/hero.png");
hero.instances.push_back({{10, 0, 10}, 0.0f, hero.sizeForDensity(32.0f)});
hero.upload();

hd2d::ModelLibrary models; models.load("assets/models/scenery.glb");
hd2d::ModelBatch oaks(models.get("oak_a"));
oaks.instances.push_back({{14, 0, 12}, /*yaw*/ 0.7f, /*scale*/ glm::vec3(1.0f), /*tint*/ 1.0f});
oaks.upload();

hd2d::Scene scene;
scene.terrain = &terrain;
scene.billboards = {&hero};
scene.models = {&oaks};

hd2d::GroundOcclusion contactAO;  // dark contact shadow at tree bases
oaks.groundOcclusion = 1.0f;
contactAO.build({0, 0}, {64, 64}, {&oaks});
scene.groundOcclusion = &contactAO;

hd2d::ParticleSystem particles;  // fire: flames, embers, smoke, glow, light
particles.addFire({12, 1.5f, 12});
scene.particles = &particles;
scene.volumetricLights = 1;      // the fire light also glows in the fog

hd2d::Weather weather;            // weather.setRain(1.0f) for a storm
hd2d::Camera cam;  cam.target = {10, 0.7f, 10};

float time = 0.0f;
while (window.pollEvents()) {
    time += 1.0f / 60.0f;
    particles.update(1.0f / 60.0f, time);
    scene.lights.clear();
    particles.collectLights(scene.lights, 12);
    weather.update(1.0f / 60.0f, scene, renderer.settings);
    renderer.render(scene, cam, time);
    window.swapBuffers();
}

demo/DemoScene.cpp is the full reference: terrain generation, prop scattering, bridge walking, fires from model sockets, contact AO, water + shore field, particles, weather, player movement and collision.

Assets

3D scenery (Blender)

Trees, bushes, rocks and the stone bridge (with braziers) are generated procedurally so they can be tweaked and rebuilt at any time:

python3 tools/scenery_textures.py          # paints assets/models/src/*.png (numpy + Pillow)
blender -b -P tools/blender_scenery.py     # builds the models, writes assets/models/scenery.glb

blender_scenery.py can also be run inside a live Blender session (e.g. through the Blender MCP) with exec(open(".../tools/blender_scenery.py").read()); it only touches its own HD2D_Scenery collection and lays the models out side by side for inspection. Any glTF works with ModelLibrary; the engine relies on a few conventions:

  • every root node with a mesh becomes a model, looked up by node name;
  • COLOR_0.rgb multiplies the base color (tint / baked AO), COLOR_0.a is the wind weight (0 rigid, 1 fully swaying);
  • materials named foliage* are alpha-tested, two-sided and get wrap lighting, translucency and soft self-shadowing; others honor glTF alphaMode and doubleSided;
  • foliage looks best with custom normals pointing away from the canopy center;
  • emissive materials (glTF emissiveFactor / emissiveTexture / KHR_materials_emissive_strength) glow and bloom;
  • child empties of a model node become named sockets (Model::sockets, model space; ModelInstance::toWorld places them). The bridge exports fire_0 / fire_1 in its braziers and the demo spawns fires there.

Pixel art (PixelLab)

tools/pixellab.py generates pixel-art textures through the PixelLab API. Put your key in .env (see .env.example); assets are declared in tools/assets.json.

python3 tools/pixellab.py --list        # what exists
python3 tools/pixellab.py               # generate missing assets
python3 tools/pixellab.py tall_grass    # regenerate one asset
python3 tools/pixellab.py --balance
  • "kind": "image" uses /create-image-pixflux (transparent sprites).
  • "kind": "tiles" uses /create-tiles-pro (seamless top-down tiles); every variation is saved as <name>_<n>.png. A "tile_id" re-downloads an existing set without paying again.

Pixel-art texel density is 32 px per world unit; the 256 px test sprite is 4× upscaled 64 px art, so it is rendered at 128 px per unit.

Roadmap

The demo covers the rendering core. Natural next steps for a full engine: sprite-sheet animation + 4/8-direction facing, a scene/level file format and editor, more weather (snow, thunderstorms with lightning, wind-blown leaves), time-of-day presets, cascaded shadows for large maps, audio, and an entity/component layer for gameplay.

Description
No description provided
Readme 28 MiB
Languages
C++ 64.7%
GLSL 19.4%
Python 14.2%
CMake 1.6%