Files
Vibe-HD-2D/README.md
Gabrieal Jimmy 16c03afd16 Rain and more
2026-10-02 13:04:49 -05:00

234 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](docs/screenshot.png)
![heavy rain](docs/rain.png)
## 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 | Tall scenery (tree canopies) between the camera and the player is dithered away around the sprite, only above the sprite's head; low props opt out per batch (`ModelBatch::occluderFade = false`, used for the bridge, rocks and bushes) |
| 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`): a compact, bowl-filling mass of nearly opaque, noise-eroded flame licks shaded per pixel from a hot yellow core through orange to red tips (drawn oldest-first so the core sits on top), 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](https://www.unrealengine.com/spotlights/octopath-traveler-s-hd-2d-art-style-and-story-make-for-a-jrpg-dream-come-true),
the [Octopath Traveler II interview](https://www.unrealengine.com/developer-interviews/octopath-traveler-ii-builds-a-bigger-bolder-world-in-its-stunning-hd-2d-style),
the Unreal Fest Europe 2019 talk ["The Fusion of Nostalgia and Novelty"](https://www.unrealengine.com/events/unreal-fest-europe-2019/the-fusion-of-nostalgia-and-novelty-in-the-development-of-octopath-traveler),
and [The Verge on HD-2D's evolution](https://www.theverge.com/entertainment/840080/square-enix-hd2d-games-octopath-dragon-quest).
## 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.
```sh
# 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):
```sh
./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
docs/ASSET_GUIDE.md how to make sprites, tiles, textures and models (for humans and AI agents)
AGENTS.md build/verify workflow and code rules for AI agents
```
### Frame pipeline (`Renderer::render`)
```mermaid
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
```cpp
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
The full guide to making sprites, terrain tiles, high-res textures and 3D
models (scale, conventions, PixelLab manifest fields, Blender helpers, how to
check results) is in [`docs/ASSET_GUIDE.md`](docs/ASSET_GUIDE.md). Summary:
### 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:
```sh
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](https://api.pixellab.ai/v2/llms.txt). Put your key in `.env`
(see `.env.example`); assets are declared in `tools/assets.json`.
```sh
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.