Sprite sheets, atlases, game UI, time of day, weather presets, headless captures
- BillboardInstance: per-instance uv sub-rect, feet anchor, turn, dithered alpha, flash; BillboardBatch::sharedTexture; Texture createEmpty/update/cellUV/cellAnchor/nearest - SpriteAtlas: fixed-cell runtime atlas with gutters - hd2d::ui: Octopath-style panels, fonts (Cinzel, EB Garamond), dialogue box with typewriter + portraits + choices, menus, nameplates, toasts, ImGui theme - DayCycle (dawn/day/dusk/night keyframes, sun + moon arcs), Weather overcast/mist/ lightning, night-aware presets, stars and moon in the sky - Hidden-window offscreen captures (Window visible=false, Renderer output FBO, OffscreenTarget); --screenshot never opens a window - Camera::worldToScreen; CMake usable via add_subdirectory (HD2D_BUILD_DEMO, HD2D_ENGINE_ASSET_DIR)
This commit is contained in:
162
README.md
162
README.md
@@ -11,6 +11,7 @@ high-resolution 3D effects.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## What makes HD-2D look like HD-2D
|
||||
|
||||
@@ -18,7 +19,7 @@ high-resolution 3D effects.
|
||||
| --- | --- |
|
||||
| 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 |
|
||||
| Upright 2D sprites in 3D | Characters and small props are camera-facing (Y-axis) billboards (`BillboardBatch`), planted by their lowest opaque pixel row (or a per-frame feet row); per instance: sprite-sheet / atlas sub-rect (`SpriteAtlas`, `Texture::cellUV`), flip, paper-turn yaw, dithered alpha fade and hit flash |
|
||||
| 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) |
|
||||
@@ -31,7 +32,9 @@ high-resolution 3D effects.
|
||||
| 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 |
|
||||
| Weather | `Weather` blends the clear look toward overcast (grey sky, weak sun, soft shadows), mist (thick low fog) and storm presets, each with its own 0..1 target and smooth transition: procedural world-space rain streaks (up to 18k, one draw call), ground splashes, raindrop ripple rings on the water, wet darker glossy surfaces, optional lightning flashes, ambient fireflies fade out |
|
||||
| Time of day | `DayCycle` turns an hour into a complete look from tweakable keyframes (night, dawn, morning, noon, afternoon, dusk): sun arc by day, a dim cool moon as the shadow-casting light at night, deep blue nights with a twinkling star field and moon disc (also in water reflections), orange / purple dusks, brighter fireflies after dark; weather presets darken with it |
|
||||
| Game UI | `hd2d::ui` (Octopath / Triangle Strategy look): translucent navy panels with a gold double border and corner gems, high-res serif text (Cinzel, EB Garamond) with a soft shadow, a typewriter dialogue box with gold name plate, portrait and choices, menus, name plates, toasts, progress bars and a matching Dear ImGui theme |
|
||||
|
||||
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),
|
||||
@@ -40,9 +43,9 @@ and [The Verge on HD-2D's evolution](https://www.theverge.com/entertainment/8400
|
||||
|
||||
## Building
|
||||
|
||||
Requirements: CMake ≥ 3.20, a C++20 compiler, OpenGL 4.5, `libepoxy`.
|
||||
Requirements: CMake ≥ 3.21, 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.
|
||||
otherwise; Dear ImGui is fetched unless a parent project provides `imgui`.
|
||||
|
||||
```sh
|
||||
# Arch / CachyOS
|
||||
@@ -56,6 +59,19 @@ ninja -C build
|
||||
The asset path is baked in at configure time (`HD2D_ASSET_DIR`), so the demo can
|
||||
be run from any working directory.
|
||||
|
||||
To use the engine from another CMake project:
|
||||
|
||||
```cmake
|
||||
add_subdirectory(path/to/Vibe-HD-2D hd2d) # HD2D_BUILD_DEMO defaults OFF here
|
||||
target_link_libraries(my_game PRIVATE hd2d)
|
||||
# Engine shaders: renderer.init(std::string(HD2D_ENGINE_ASSET_DIR) + "/shaders", ...)
|
||||
```
|
||||
|
||||
`hd2d` publicly defines `HD2D_ENGINE_ASSET_DIR` (also a CMake cache variable)
|
||||
pointing at the engine's `assets/`. The engine provides the `imgui` target
|
||||
(Dear ImGui + GLFW/OpenGL3 backends) unless the parent defines one first;
|
||||
`glfw` and `glm::glm` targets from the parent are reused too.
|
||||
|
||||
## Demo controls
|
||||
|
||||
| Input | Action |
|
||||
@@ -65,8 +81,12 @@ be run from any working directory.
|
||||
| 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) |
|
||||
| O / G | Toggle overcast / morning mist |
|
||||
| L | Toggle lightning (flashes while raining) |
|
||||
| [ / ] | Time of day −/+ 1 hour (switches the demo to `DayCycle`) |
|
||||
| F5 | Hot-reload shaders |
|
||||
| F12 | Screenshot to `screenshots/` |
|
||||
| T | Toggle the game UI showcase (Enter / Space advance the dialogue, Up / Down pick a choice) |
|
||||
| Esc | Quit |
|
||||
|
||||
Scriptable captures (useful for look-dev and regression checks):
|
||||
@@ -76,7 +96,31 @@ Scriptable captures (useful for look-dev and regression checks):
|
||||
--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:
|
||||
Captures run hidden: `--screenshot` implies a hidden window (no toplevel is
|
||||
ever created, so nothing pops up or takes focus) rendering into an offscreen
|
||||
target, and all keyboard / mouse input is ignored, so captures can run while
|
||||
you work on the same desktop. `--show` opts back into a visible capture
|
||||
window (input still ignored). Hidden and visible captures match to within
|
||||
1/255 on a handful of pixels (driver rounding of the window surface).
|
||||
|
||||
Any app can capture headlessly the same way:
|
||||
|
||||
```cpp
|
||||
window.create(w, h, "capture", false, false, /*visible*/ false); // swapBuffers no-op, input reports nothing
|
||||
hd2d::OffscreenTarget out; out.create(window.width(), window.height()); // RGBA8 + depth/stencil
|
||||
renderer.setOutputFramebuffer(out.fbo()); // tonemap writes here and leaves it bound for ImGui / ui::*
|
||||
renderer.render(scene, cam, time); /* ... UI ... */
|
||||
out.save("shot.png"); // = hd2d::saveScreenshot(path, w, h, out.fbo())
|
||||
```
|
||||
|
||||
`--rain 0..1`, `--overcast 0..1` and `--fog 0..1` start the capture already
|
||||
at that weather (`--lightning` enables flashes); `--hour H` drives the look
|
||||
from `DayCycle` (without it the scene keeps its hand-tuned look); `--pitch deg`
|
||||
sets the camera tilt (low values show the sky). `--sprite-test` adds a
|
||||
labelled row of atlas / sprite-sheet billboards (sub-rect frames, flip, turn,
|
||||
alpha dither, flash) in front of the player. `--ui-demo` opens the game UI
|
||||
showcase (same as T): `--frames 120` captures the dialogue mid-reveal,
|
||||
`--frames 420` fully revealed with its choices. `--set` accepts:
|
||||
`volumetrics dof bloom reflections occluderFade ssao groundAO ssaoRadius
|
||||
ssaoIntensity fogDensity fogHeightFalloff godRays shaftStrength shaftCoverage
|
||||
shaftScale cookieStrength exposure bloomStrength tiltShift focusRange`.
|
||||
@@ -88,6 +132,7 @@ 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/fonts/ OFL fonts for hd2d::ui (Cinzel, Cinzel Decorative, EB Garamond) + licenses
|
||||
assets/sprites/ character sprites
|
||||
assets/models/ scenery.glb (trees, bushes, rocks, bridge) + src/ painted textures
|
||||
demo/ meadow demo scene + main loop
|
||||
@@ -129,6 +174,16 @@ hd2d::BillboardBatch hero; hero.loadTexture("assets/sprites/hero.png");
|
||||
hero.instances.push_back({{10, 0, 10}, 0.0f, hero.sizeForDensity(32.0f)});
|
||||
hero.upload();
|
||||
|
||||
// Sprite sheets / runtime atlases: one sub-rect (`uv`) + feet row (`anchor`)
|
||||
// per instance; `flip`, `turn`, `alpha` (dithered) and `flash` per instance too.
|
||||
hd2d::SpriteAtlas atlas; atlas.init({64, 64}, 8, 8);
|
||||
hd2d::BillboardBatch critters; critters.sharedTexture = &atlas.texture();
|
||||
int slot = atlas.add(image); // hd2d::Image, e.g. from hd2d::loadImage
|
||||
hd2d::BillboardInstance c;
|
||||
c.pos = {12, 0, 9}; c.size = hd2d::BillboardBatch::cellSize({64, 64}, 32.0f);
|
||||
c.uv = atlas.uv(slot); c.anchor = atlas.anchor(slot);
|
||||
critters.instances.push_back(c); critters.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});
|
||||
@@ -136,7 +191,7 @@ oaks.upload();
|
||||
|
||||
hd2d::Scene scene;
|
||||
scene.terrain = &terrain;
|
||||
scene.billboards = {&hero};
|
||||
scene.billboards = {&hero, &critters};
|
||||
scene.models = {&oaks};
|
||||
|
||||
hd2d::GroundOcclusion contactAO; // dark contact shadow at tree bases
|
||||
@@ -168,8 +223,98 @@ while (window.pollEvents()) {
|
||||
scattering, bridge walking, fires from model sockets, contact AO, water +
|
||||
shore field, particles, weather, player movement and collision.
|
||||
|
||||
### Time of day and weather (`hd2d/DayCycle.h`, `hd2d/Weather.h`)
|
||||
|
||||
Both are meant to be driven externally, e.g. by a game server that sends the
|
||||
in-game `hour` and the current weather. `DayCycle::look(hour)` returns a
|
||||
`WeatherLook` (sun / moon direction, intensity and color, sky colors, ambient,
|
||||
fog, god rays, bloom, exposure / saturation / lift / gain, star + moon
|
||||
visibility, firefly glow); `Weather` blends it toward its presets and applies
|
||||
the result to the `Scene` and `RenderSettings`.
|
||||
|
||||
```cpp
|
||||
hd2d::DayCycle day; // default keyframes; tweak day.keys[i].look, sunrise, sunset, moon arc
|
||||
hd2d::Weather weather;
|
||||
|
||||
void onServerState(float hour, const std::string& kind) { // e.g. "clear", "overcast", "fog", "rain", "storm"
|
||||
serverHour = hour; // [0, 24); interpolate between updates if you like
|
||||
weather.setRain(kind == "rain" || kind == "storm" ? 1.0f : 0.0f);
|
||||
weather.setOvercast(kind == "overcast" ? 1.0f : 0.0f);
|
||||
weather.setFog(kind == "fog" ? 1.0f : 0.0f);
|
||||
weather.setLightning(kind == "storm");
|
||||
}
|
||||
// once after joining: weather.snap(); (skip the transition)
|
||||
|
||||
// every frame, before renderer.render():
|
||||
weather.setClearLook(day.look(serverHour));
|
||||
weather.update(dt, scene, renderer.settings);
|
||||
```
|
||||
|
||||
| API | Notes |
|
||||
| --- | --- |
|
||||
| `DayCycle::look(hour)` | Smooth (smoothstep) interpolation between `keys` (wraps at midnight); the light follows a sun arc between `sunrise` and `sunset` and a moon arc otherwise, fading out near the horizon so the sun → moon switch is invisible |
|
||||
| `DayCycle::daylight(hour)` / `night(hour)` | 0..1 helpers: sun height (0 at night) and star / moon visibility, e.g. for gameplay (spawn tables, NPC schedules) |
|
||||
| `Weather::setClearLook(look)` | Replaces the clear look every frame; without it `Weather` captures the scene's own look while clear (manual tuning survives) |
|
||||
| `setRain` / `setOvercast` / `setFog` (0..1) | Targets reached over `transitionSeconds`; `snap()` jumps to all of them |
|
||||
| `setLightning(bool)` | Random double flashes (sky, sun, fog) while rain > 0.5, about every `lightningInterval` s |
|
||||
| `storm`, `overcastLook`, `mist` | Presets authored at full daylight; darkened by the clear look's `daylight` toward `nightBrightness` and tinted toward its sky at night, so a midnight storm is dark blue-grey, not a bright grey sky. Sun direction, exposure, grading and fireflies always come from the clear look |
|
||||
| `ParticleSystem::fireflyGlow` | Firefly brightness + light; set from `WeatherLook::fireflyGlow` (the default night keys raise it). `ambientFade` still fades fireflies and motes in rain / mist |
|
||||
|
||||
Night look: the "sun" becomes the moon (direction, color, intensity), so
|
||||
shadows keep working; `Sky::night` turns on the star field and moon disc in
|
||||
`sky.frag` (hidden by clouds, `Sky::clouds` / rain). Point lights (fires,
|
||||
fireflies) are absolute HDR, so they read strongly once the ambient drops.
|
||||
With `DayCycle` driving the look, the sun / sky / fog / grading sliders of the
|
||||
renderer panel are overwritten every frame: tune the keyframes instead (F1 →
|
||||
Weather → Day keyframes).
|
||||
|
||||
### Game UI (`hd2d/UI.h`)
|
||||
|
||||
`hd2d::ui` draws an HD-2D JRPG interface with Dear ImGui draw lists (no ImGui
|
||||
windows needed), so it composites over the final frame like the debug panels.
|
||||
All sizes are authored for a 1080 px tall viewport and scaled by
|
||||
`ui::scaleFor(viewport)`.
|
||||
|
||||
```cpp
|
||||
hd2d::DebugUI imgui; imgui.init(window); // creates the ImGui context
|
||||
namespace ui = hd2d::ui;
|
||||
ImVec2 vp(window.width(), window.height());
|
||||
ui::loadFonts(std::string(HD2D_ENGINE_ASSET_DIR) + "/fonts", ui::scaleFor(vp)); // before the first frame
|
||||
ui::applyTheme(); // optional: restyle standard ImGui widgets
|
||||
|
||||
ui::DialogueBox box;
|
||||
box.start({{"Warden", "Welcome, traveller!", portraitTexId}}, {"Yes", "No"});
|
||||
ui::Toasts toasts; toasts.push("Field guide updated");
|
||||
|
||||
// every frame, between imgui.beginFrame() and imgui.endFrame():
|
||||
if (confirmPressed && box.advance()) onChoice(box.choice()); // -1 without choices
|
||||
if (upPressed) box.moveChoice(-1);
|
||||
box.update(dt); box.draw(vp);
|
||||
toasts.update(dt); toasts.draw(vp);
|
||||
ImDrawList* dl = ImGui::GetForegroundDrawList();
|
||||
ui::menu(dl, {vp.x * 0.15f, vp.y * 0.4f}, {"Items", "Party", "Quit"}, selected, "Menu", ui::scaleFor(vp));
|
||||
ui::nameplate(dl, headPx, "Traveller", ui::kGold, ui::scaleFor(vp)); // headPx from Camera::worldToScreen
|
||||
```
|
||||
|
||||
| Piece | Notes |
|
||||
| --- | --- |
|
||||
| `loadFonts(dir, pixelScale)` | Bakes `body` / `small` (EB Garamond), `name` (Cinzel) and `title` (Cinzel Decorative Bold) at `kBodySize`… × `pixelScale` so text is pixel-exact; ImGui's default font stays the default for debug panels. Call outside a frame; call again after a resize to rebake |
|
||||
| `panel`, `PanelStyle` | Navy → near-black gradient, soft drop shadow, gold double border, corner gems + brackets (`ornate`), `tint` alpha fades everything |
|
||||
| `text`, `textWrapped`, `textCentered`, `measure` | Pixel-snapped text with a 1 px dark shadow (plus a faint halo); null font = current ImGui font |
|
||||
| `titleText` | Gold gradient logo with dark halo, warm glow, outline and a gem flourish rule |
|
||||
| `nameplate` | Pill label whose pointer tip sits on a screen point |
|
||||
| `menu`, `menuAt`, `menuSize` | Option list panel with a gold highlight band and bobbing cursor |
|
||||
| `progressBar` | Recessed rounded track with a glossy gradient fill |
|
||||
| `DialogueBox` | Bottom centre, 70 % width, word-wrapped from the full line so words never jump, UTF-8 typewriter at `charsPerSecond` with `sentencePause` / `clausePause`, optional portrait (`ImTextureID` + UVs; load pixel art with `Texture::load(path, false, true)` for crisp nearest filtering), gold speaker plate, blinking gem when revealed, choices above the right end |
|
||||
| `Toasts` | Banners stacked at the top centre; slide in, fade out after `seconds` |
|
||||
|
||||
## Assets
|
||||
|
||||
Fonts in `assets/fonts/` come from [google/fonts](https://github.com/google/fonts)
|
||||
under the SIL Open Font License (`*-OFL.txt` next to each font; files renamed
|
||||
from `Family[wght].ttf`). Variable fonts are rasterised at their default
|
||||
(Regular) instance.
|
||||
|
||||
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:
|
||||
@@ -227,7 +372,6 @@ 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
|
||||
animation playback + 4/8-direction facing on top of the sheet/atlas frames, a scene/level file format and
|
||||
editor, more weather (snow, wind-blown leaves), cascaded shadows for large maps, audio, and an
|
||||
entity/component layer for gameplay.
|
||||
|
||||
Reference in New Issue
Block a user