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:
Gabrieal Jimmy
2026-10-02 15:05:23 -05:00
parent 16c03afd16
commit 83a1f16806
44 changed files with 2835 additions and 226 deletions

View File

@@ -141,9 +141,111 @@ bool DemoScene::load(const std::string& assetDir) {
player_.instances.resize(1);
placePlayer(kStart);
return !spriteTest || buildSpriteTest(assetDir);
}
namespace {
// Lowest row (from the bottom) with alpha >= 128, or 0 if none.
int lowestOpaqueRow(const Image& img) {
for (int y = 0; y < img.height; ++y)
for (int x = 0; x < img.width; ++x)
if (img.rgba[(size_t(y) * img.width + x) * 4 + 3] >= 128) return y;
return 0;
}
// Copies `src` into `dst` with its bottom-left pixel at (x, y).
void blit(Image& dst, const Image& src, int x, int y) {
for (int r = 0; r < src.height; ++r)
std::copy_n(&src.rgba[size_t(r) * src.width * 4], size_t(src.width) * 4,
&dst.rgba[(size_t(y + r) * dst.width + x) * 4]);
}
} // namespace
bool DemoScene::buildSpriteTest(const std::string& assetDir) {
Image big, flowers, tuft;
if (!loadImage(assetDir + "/sprites/test_sprite.png", big) || !loadImage(assetDir + "/textures/flowers.png", flowers) ||
!loadImage(assetDir + "/textures/grass_tuft.png", tuft))
return false;
// The test sprite is 4x upscaled 64 px art: take one pixel per 4x4 block.
Image hero;
hero.width = big.width / 4;
hero.height = big.height / 4;
hero.rgba.resize(size_t(hero.width) * hero.height * 4);
for (int y = 0; y < hero.height; ++y)
for (int x = 0; x < hero.width; ++x)
std::copy_n(&big.rgba[((size_t(y) * 4 + 2) * big.width + x * 4 + 2) * 4], 4,
&hero.rgba[(size_t(y) * hero.width + x) * 4]);
// Runtime atlas: add, then exercise remove/re-add so slots get reused.
if (!atlas_.init({64, 64}, 4, 2)) return false;
atlasSlots_[0] = atlas_.add(hero);
int scratch = atlas_.add(big); // cropped to the cell
atlas_.remove(scratch);
atlasSlots_[1] = atlas_.add(flowers); // reuses the freed slot
atlasSlots_[2] = atlas_.add(tuft);
HD2D_LOG("sprite test atlas: %d/%d slots used", atlas_.used(), atlas_.capacity());
atlasSprites_.sharedTexture = &atlas_.texture();
atlasSprites_.instances.resize(6);
// 2x1 sprite sheet (authored top-down, one row): hero | flowers.
Image sheet;
sheet.width = 128;
sheet.height = 64;
sheet.rgba.assign(size_t(sheet.width) * sheet.height * 4, 0);
blit(sheet, hero, 0, 0);
blit(sheet, flowers, 64 + (64 - flowers.width) / 2, 0);
bleedTransparentColors(sheet);
if (!sheetSprites_.texture.create(sheet)) return false;
sheetFeetRows_[0] = 63 - lowestOpaqueRow(hero);
sheetFeetRows_[1] = 63 - lowestOpaqueRow(flowers);
sheetSprites_.instances.resize(1);
atlasSprites_.emissive = sheetSprites_.emissive = player_.emissive;
scene.billboards.push_back(&atlasSprites_);
scene.billboards.push_back(&sheetSprites_);
return true;
}
void DemoScene::updateSpriteTest(float time, const Camera& camera) {
glm::vec3 right = camera.right();
glm::vec3 fwd(right.z, 0.0f, -right.x); // horizontal camera forward
const glm::vec2 size = BillboardBatch::cellSize(atlas_.cellPixels(), kPixelsPerUnit);
labels.clear();
auto place = [&](BillboardInstance& bi, int index, const char* label) {
glm::vec3 p = playerPos - fwd * 1.8f + right * (float(index) - 3.0f); // row in front of the player
bi = BillboardInstance{};
bi.pos = glm::vec3(p.x, groundHeight({p.x, p.z}), p.z);
bi.size = size;
labels.push_back({bi.pos + glm::vec3(0, size.y + 0.15f, 0), label});
};
auto useSlot = [&](BillboardInstance& bi, int slot) {
bi.uv = atlas_.uv(slot);
bi.anchor = atlas_.anchor(slot);
};
auto& a = atlasSprites_.instances;
const char* names[] = {"atlas", "flip", "turn", "alpha", "flash", "frames"};
for (int i = 0; i < 6; ++i) {
place(a[i], i, names[i]);
useSlot(a[i], atlasSlots_[0]);
}
a[1].flip = 1.0f;
a[2].turn = 0.8f + 0.4f * std::sin(time * 1.5f);
a[3].alpha = 0.5f + 0.3f * std::sin(time * 2.0f);
a[4].flash = 0.5f + 0.5f * std::sin(time * 5.0f);
useSlot(a[5], atlasSlots_[int(time / 0.4f) % 3]);
atlasSprites_.upload();
BillboardInstance& s = sheetSprites_.instances[0];
place(s, 6, "sheet");
int frame = int(time / 0.5f) % 2;
glm::ivec2 texSize = sheetSprites_.texture.size();
s.uv = Texture::cellUV(texSize, {64, 64}, frame, 0);
s.anchor = Texture::cellAnchor(texSize, {64, 64}, 0, sheetFeetRows_[frame]);
sheetSprites_.upload();
}
void DemoScene::buildTerrain() {
Terrain& t = terrain_;
t.init(kW, kD, 0.5f);
@@ -398,4 +500,5 @@ void DemoScene::update(float dt, float time, glm::vec2 move, const Camera& camer
scene.interactor = playerPos;
scene.blobShadowPos = glm::vec3(playerPos.x, ground, playerPos.z);
scene.focusPoint = playerPos + glm::vec3(0, 0.6f, 0);
if (spriteTest) updateSpriteTest(time, camera);
}

View File

@@ -1,6 +1,7 @@
#pragma once
#include "hd2d/Camera.h"
#include "hd2d/Scene.h"
#include "hd2d/SpriteAtlas.h"
#include "hd2d/Weather.h"
#include <array>
@@ -21,11 +22,24 @@ public:
glm::vec3 playerPos{0.0f};
hd2d::Weather weather; // call weather.update() each frame before rendering
// Billboard feature test (`--sprite-test`; set before load()): a row of
// atlas / sprite-sheet sprites behind the player exercising sub-rect
// frames, flip, turn, alpha dither and flash. Labels are world-space
// points (sprite tops) for the caller to draw.
bool spriteTest = false;
struct Label {
glm::vec3 pos;
const char* text;
};
std::vector<Label> labels;
private:
struct Obstacle {
glm::vec2 pos;
float radius;
};
bool buildSpriteTest(const std::string& assetDir);
void updateSpriteTest(float time, const hd2d::Camera& camera);
void buildTerrain();
void scatterProps();
bool walkable(glm::vec2 from, glm::vec2 to) const;
@@ -45,6 +59,11 @@ private:
std::mt19937 rng_{42};
float playerSpeed_ = 4.2f;
float walkPhase_ = 0.0f;
hd2d::SpriteAtlas atlas_; // sprite test: runtime-packed cells
hd2d::BillboardBatch atlasSprites_; // uses atlas_.texture() via sharedTexture
hd2d::BillboardBatch sheetSprites_; // own texture: a 2x1 sheet read with Texture::cellUV
int atlasSlots_[3] = {-1, -1, -1}; // hero, flowers, tuft
int sheetFeetRows_[2] = {0, 0}; // per sheet cell, from the cell top
float stepTimer_ = 0.0f;
float facing_ = 0.0f;
};

View File

@@ -1,18 +1,26 @@
// HD-2D engine demo: meadow diorama with a WASD-controlled sprite.
//
// Controls: WASD/arrows move, Q/E rotate camera, right-drag orbit, wheel zoom,
// F1 toggle tuning panel, F5 hot-reload shaders, F12 screenshot, Esc quit.
// R rain, O overcast, G fog, L lightning, [ / ] time of day -/+ 1 h,
// F1 toggle tuning panel, F5 hot-reload shaders, F12 screenshot, T game UI showcase, Esc quit.
//
// Headless-ish capture for testing:
// hd2d_demo --screenshot out.png [--frames 120] [--move x,y] [--pos x,z]
// Captures run hidden: --screenshot implies a hidden, input-ignoring window
// rendering into an offscreen target (--show opts back into a visible window;
// input is still ignored):
// hd2d_demo --screenshot out.png [--frames 120] [--move x,y] [--pos x,z] [--show]
// [--sun azimuthDeg,elevationDeg] [--yaw deg] [--dist units] [--size WxH] [--ui]
// [--set name=value ...] e.g. --set godRays=4 --set dof=0
// [--hour H] [--rain X] [--overcast X] [--fog X] [--lightning] [--pitch deg]
// [--sprite-test] billboard feature row (atlas, flip, turn, alpha, flash, frames)
// [--ui-demo] game UI showcase (also key T): dialogue, choices, toasts, menu, nameplate
#include "DemoScene.h"
#include "hd2d/DayCycle.h"
#include "hd2d/DebugUI.h"
#include "hd2d/Log.h"
#include "hd2d/Renderer.h"
#include "hd2d/Screenshot.h"
#include "hd2d/UI.h"
#include "hd2d/Window.h"
#include <GLFW/glfw3.h>
@@ -38,10 +46,11 @@ struct Options {
std::string screenshot;
int frames = 120;
glm::vec2 move{0.0f};
bool hasPos = false, hasSun = false, hasYaw = false, ui = false;
bool hasPos = false, hasSun = false, hasYaw = false, ui = false, spriteTest = false, uiDemo = false, show = false;
bool hasHour = false, hasPitch = false, lightning = false;
std::vector<std::pair<std::string, float>> settings; // --set name=value
glm::vec2 pos{0.0f}, sun{0.0f};
float yaw = 0.0f, dist = 0.0f, rain = 0.0f;
float yaw = 0.0f, pitch = 0.0f, dist = 0.0f, rain = 0.0f, overcast = 0.0f, fog = 0.0f, hour = 12.0f;
int width = 1600, height = 900;
};
@@ -63,7 +72,15 @@ Options parseArgs(int argc, char** argv) {
else if (!std::strcmp(argv[i], "--yaw")) o.hasYaw = true, o.yaw = float(std::atof(next()));
else if (!std::strcmp(argv[i], "--dist")) o.dist = float(std::atof(next()));
else if (!std::strcmp(argv[i], "--ui")) o.ui = true;
else if (!std::strcmp(argv[i], "--show")) o.show = true;
else if (!std::strcmp(argv[i], "--sprite-test")) o.spriteTest = true;
else if (!std::strcmp(argv[i], "--ui-demo")) o.uiDemo = true;
else if (!std::strcmp(argv[i], "--rain")) o.rain = float(std::atof(next()));
else if (!std::strcmp(argv[i], "--overcast")) o.overcast = float(std::atof(next()));
else if (!std::strcmp(argv[i], "--fog")) o.fog = float(std::atof(next()));
else if (!std::strcmp(argv[i], "--lightning")) o.lightning = true;
else if (!std::strcmp(argv[i], "--hour")) o.hasHour = true, o.hour = float(std::atof(next()));
else if (!std::strcmp(argv[i], "--pitch")) o.hasPitch = true, o.pitch = float(std::atof(next()));
else if (!std::strcmp(argv[i], "--set")) {
std::string kv = next();
auto eq = kv.find('=');
@@ -111,16 +128,124 @@ std::string timestampedScreenshotPath() {
return buf;
}
// Game UI showcase (key T or --ui-demo): typewriter dialogue with a portrait
// and choices, toasts, a menu, a status panel, a title, a themed ImGui window
// and a nameplate above the player. The ImGui theme is restored on exit.
struct UiShowcase {
bool active = false;
hd2d::ui::DialogueBox dialogue;
hd2d::ui::Toasts toasts;
hd2d::Texture portrait;
ImGuiStyle savedStyle;
float volume = 0.7f; // state for the themed "Options" window
bool subtitles = true;
int textSpeed = 1;
char playerName[32] = "Traveller";
void open() {
if (!active) savedStyle = ImGui::GetStyle();
hd2d::ui::applyTheme();
active = true;
glm::vec4 b = portrait.opaqueBounds(); // square crop of the sprite's head and shoulders
float side = b.z - b.x;
dialogue.start({{"Warden Elise",
"Ah, a new face! Welcome to the meadow, traveller \xE2\x80\x94 the tall grass to the east hides "
"all manner of curious creatures, and the caf\xC3\xA9 by the bridge sells the finest bait\xE2\x80\xA6 "
"Will you take a field guide with you?",
ImTextureID(portrait.id()), ImVec2(b.x, b.y), ImVec2(b.z, b.y + side)}},
{"Yes, gladly.", "Not today.", "Tell me more first."});
toasts.push("Field guide updated", IM_COL32(230, 190, 90, 255), 9.0f);
toasts.push("New area: Sunlit Meadow", IM_COL32(120, 200, 160, 255), 9.0f);
}
void close() {
dialogue.close();
toasts.clear();
ImGui::GetStyle() = savedStyle;
active = false;
}
// Enter/Space advance, Up/Down pick a choice; Enter reopens a closed dialogue.
void input(hd2d::Window& w) {
if (dialogue.isOpen()) {
if (w.keyPressed(GLFW_KEY_UP) || w.keyPressed(GLFW_KEY_W)) dialogue.moveChoice(-1);
if (w.keyPressed(GLFW_KEY_DOWN) || w.keyPressed(GLFW_KEY_S)) dialogue.moveChoice(1);
if ((w.keyPressed(GLFW_KEY_ENTER) || w.keyPressed(GLFW_KEY_SPACE)) && dialogue.advance()) {
static const char* answers[] = {"Received a field guide", "Maybe next time", "Ask again with T"};
int c = dialogue.choice();
if (c >= 0) toasts.push(answers[c]);
}
} else if (w.keyPressed(GLFW_KEY_ENTER)) {
open();
}
}
void draw(glm::vec2 viewport, const hd2d::Camera& cam, glm::vec3 playerPos, float dt) {
namespace ui = hd2d::ui;
const ImVec2 vp(viewport.x, viewport.y);
const float s = ui::scaleFor(vp);
ImDrawList* dl = ImGui::GetForegroundDrawList();
dialogue.update(dt);
toasts.update(dt);
ui::titleText(dl, ImVec2(vp.x * 0.5f, vp.y * 0.25f), "Meadow Tales", ui::kTitleSize * s);
ui::menu(dl, ImVec2(vp.x * 0.13f, vp.y * 0.42f), {"Items", "Party", "Field Guide", "Settings", "Quit"}, 2,
"Menu", s);
// Status panel with progress bars (top right).
ImVec2 p0(vp.x - 380.0f * s - 40.0f * s, 40.0f * s), p1(vp.x - 40.0f * s, 40.0f * s + 150.0f * s);
ui::panel(dl, p0, p1);
ui::text(dl, ui::fonts().name, ui::kNameSize * s, ImVec2(p0.x + 28.0f * s, p0.y + 20.0f * s), ui::kGold,
"Traveller");
ui::text(dl, ui::fonts().small, ui::kSmallSize * s, ImVec2(p1.x - 110.0f * s, p0.y + 22.0f * s),
ui::kTextMuted, "Lv. 12");
const char* labels[] = {"HP", "SP"};
const ImU32 colors[] = {IM_COL32(110, 190, 90, 255), IM_COL32(80, 150, 230, 255)};
const float values[] = {0.82f, 0.45f};
for (int i = 0; i < 2; ++i) {
float y = p0.y + (68.0f + 38.0f * float(i)) * s;
ui::text(dl, ui::fonts().small, ui::kSmallSize * s, ImVec2(p0.x + 28.0f * s, y - 4.0f * s), ui::kTextColor,
labels[i]);
ui::progressBar(dl, ImVec2(p0.x + 80.0f * s, y + 2.0f * s), ImVec2(p1.x - 28.0f * s, y + 16.0f * s),
values[i], colors[i]);
}
glm::vec2 head;
if (cam.worldToScreen(playerPos + glm::vec3(0.0f, 1.45f, 0.0f), viewport, head))
ui::nameplate(dl, ImVec2(head.x, head.y), "Traveller", ui::kGold, s);
// Standard widgets with applyTheme() and the game's small font.
ImGui::PushFont(ui::fonts().small);
ImGui::SetNextWindowPos(ImVec2(vp.x - 40.0f * s, vp.y * 0.36f), ImGuiCond_Always, ImVec2(1.0f, 0.0f));
ImGui::Begin("Options", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings);
ImGui::PushItemWidth(200.0f * s);
ImGui::SliderFloat("Volume", &volume, 0.0f, 1.0f);
ImGui::Checkbox("Subtitles", &subtitles);
ImGui::Combo("Text speed", &textSpeed, "Slow\0Normal\0Fast\0");
ImGui::InputText("Name", playerName, sizeof playerName);
ImGui::Button("Apply");
ImGui::PopItemWidth();
ImGui::End();
ImGui::PopFont();
dialogue.draw(vp);
toasts.draw(vp);
}
};
// Owns every GL object so they are destroyed while the context still exists.
int run(hd2d::Window& window, const Options& opt) {
const bool capture = !opt.screenshot.empty();
hd2d::Renderer renderer;
if (!renderer.init(std::string(HD2D_ASSET_DIR) + "/shaders", window.width(), window.height())) return 1;
hd2d::OffscreenTarget offscreen; // hidden captures render here instead of the backbuffer
if (!window.visible()) {
if (!offscreen.create(window.width(), window.height())) return 1;
renderer.setOutputFramebuffer(offscreen.fbo());
}
for (const auto& [name, value] : opt.settings)
if (!applySetting(renderer.settings, name, value)) HD2D_ERR("unknown setting '%s'", name.c_str());
DemoScene demo;
demo.spriteTest = opt.spriteTest;
if (!demo.load(HD2D_ASSET_DIR)) {
HD2D_ERR("failed to load demo assets from %s", HD2D_ASSET_DIR);
return 1;
@@ -130,17 +255,31 @@ int run(hd2d::Window& window, const Options& opt) {
demo.scene.sun.azimuth = glm::radians(opt.sun.x);
demo.scene.sun.elevation = glm::radians(opt.sun.y);
}
if (opt.rain > 0.0f) {
demo.weather.setRain(opt.rain);
demo.weather.snap(); // captures start already raining
}
// Weather targets; captures start already at them.
demo.weather.setRain(opt.rain);
demo.weather.setOvercast(opt.overcast);
demo.weather.setFog(opt.fog);
demo.weather.setLightning(opt.lightning);
demo.weather.snap();
// Time of day: off (the scene's own look) until --hour, [ / ] or the hour slider.
hd2d::DayCycle dayCycle;
bool useDayCycle = opt.hasHour;
float hour = opt.hasHour ? opt.hour : 12.0f;
hd2d::DebugUI ui;
if (!ui.init(window)) return 1;
bool showUI = capture ? opt.ui : true;
if (capture) ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouse | ImGuiConfigFlags_NoKeyboard;
// Game UI fonts (ImGui's default font stays the default for the panels above).
const auto viewportOf = [&] { return ImVec2(float(window.width()), float(window.height())); };
hd2d::ui::loadFonts(std::string(HD2D_ASSET_DIR) + "/fonts", hd2d::ui::scaleFor(viewportOf()));
UiShowcase showcase;
showcase.portrait.load(std::string(HD2D_ASSET_DIR) + "/sprites/test_sprite.png", false, true);
if (opt.uiDemo) showcase.open();
hd2d::Camera cam;
if (opt.hasYaw) cam.yaw = glm::radians(opt.yaw);
if (opt.hasPitch) cam.pitch = glm::radians(opt.pitch);
if (opt.dist > 0.0f) cam.distance = opt.dist;
cam.target = demo.playerPos + glm::vec3(0, 0.7f, 0);
@@ -156,10 +295,14 @@ int run(hd2d::Window& window, const Options& opt) {
if (capture) dt = 1.0f / 60.0f; // deterministic captures
time += dt;
if (window.resized()) renderer.resize(window.width(), window.height());
if (window.resized()) {
renderer.resize(window.width(), window.height());
if (offscreen.fbo()) offscreen.resize(window.width(), window.height());
hd2d::ui::loadFonts(std::string(HD2D_ASSET_DIR) + "/fonts", hd2d::ui::scaleFor(viewportOf()));
}
glm::vec2 move = opt.move;
if (!ui.wantsKeyboard()) {
if (!capture && !ui.wantsKeyboard()) { // captures ignore the user's input
if (window.keyDown(GLFW_KEY_W) || window.keyDown(GLFW_KEY_UP)) move.y += 1.0f;
if (window.keyDown(GLFW_KEY_S) || window.keyDown(GLFW_KEY_DOWN)) move.y -= 1.0f;
if (window.keyDown(GLFW_KEY_D) || window.keyDown(GLFW_KEY_RIGHT)) move.x += 1.0f;
@@ -169,9 +312,18 @@ int run(hd2d::Window& window, const Options& opt) {
if (window.keyPressed(GLFW_KEY_F1)) showUI = !showUI;
if (window.keyPressed(GLFW_KEY_F5)) renderer.reloadShaders();
if (window.keyPressed(GLFW_KEY_R)) demo.weather.setRain(demo.weather.targetRain() > 0.0f ? 0.0f : 1.0f);
if (window.keyPressed(GLFW_KEY_O))
demo.weather.setOvercast(demo.weather.targetOvercast() > 0.0f ? 0.0f : 1.0f);
if (window.keyPressed(GLFW_KEY_G)) demo.weather.setFog(demo.weather.targetFog() > 0.0f ? 0.0f : 1.0f);
if (window.keyPressed(GLFW_KEY_L)) demo.weather.setLightning(!demo.weather.lightning());
if (window.keyPressed(GLFW_KEY_LEFT_BRACKET)) hour = std::fmod(hour + 23.0f, 24.0f), useDayCycle = true;
if (window.keyPressed(GLFW_KEY_RIGHT_BRACKET)) hour = std::fmod(hour + 1.0f, 24.0f), useDayCycle = true;
if (window.keyPressed(GLFW_KEY_ESCAPE)) window.requestClose();
if (window.keyPressed(GLFW_KEY_T)) showcase.active ? showcase.close() : showcase.open();
if (showcase.active) showcase.input(window);
if (showcase.dialogue.isOpen()) move = opt.move; // no walking while talking
}
if (!ui.wantsMouse()) {
if (!capture && !ui.wantsMouse()) {
cam.distance = std::clamp(cam.distance * std::pow(0.9f, window.scrollDelta()), 8.0f, 45.0f);
if (window.mouseDown(GLFW_MOUSE_BUTTON_RIGHT)) {
glm::vec2 d = window.mouseDelta();
@@ -184,21 +336,37 @@ int run(hd2d::Window& window, const Options& opt) {
glm::vec3 desired = demo.playerPos + glm::vec3(0, 0.7f, 0);
cam.target += (desired - cam.target) * (capture ? 1.0f : std::min(1.0f, dt * 6.0f));
if (useDayCycle) demo.weather.setClearLook(dayCycle.look(hour));
demo.weather.update(dt, demo.scene, renderer.settings);
renderer.render(demo.scene, cam, time);
if (showUI) {
if (showUI || opt.spriteTest || showcase.active) {
ui.beginFrame();
hd2d::DebugUI::renderSettingsPanel(renderer.settings, demo.scene, renderer.gpuFrameMs());
hd2d::DebugUI::weatherPanel(demo.weather);
ImGui::SetNextWindowPos(ImVec2(float(window.width()) - 12.0f, 12.0f), ImGuiCond_FirstUseEver,
ImVec2(1.0f, 0.0f));
ImGui::SetNextWindowBgAlpha(0.5f);
ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar);
ImGui::TextUnformatted("WASD move | Q/E rotate | RMB orbit | wheel zoom");
ImGui::TextUnformatted("R toggle rain | F1 panel | F5 reload shaders | F12 screenshot");
ImGui::Text("player %.1f %.1f %.1f", demo.playerPos.x, demo.playerPos.y, demo.playerPos.z);
ImGui::End();
if (showUI) {
hd2d::DebugUI::renderSettingsPanel(renderer.settings, demo.scene, renderer.gpuFrameMs());
const float prevHour = hour;
hd2d::DebugUI::weatherPanel(demo.weather, &dayCycle, &hour);
if (hour != prevHour) useDayCycle = true;
ImGui::SetNextWindowPos(ImVec2(float(window.width()) - 12.0f, 12.0f), ImGuiCond_FirstUseEver,
ImVec2(1.0f, 0.0f));
ImGui::SetNextWindowBgAlpha(0.5f);
ImGui::Begin("Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar);
ImGui::TextUnformatted("WASD move | Q/E rotate | RMB orbit | wheel zoom");
ImGui::TextUnformatted("R rain | O overcast | G fog | L lightning | [ ] hour -/+");
ImGui::TextUnformatted("F1 panel | F5 reload shaders | F12 screenshot");
ImGui::Text("player %.1f %.1f %.1f", demo.playerPos.x, demo.playerPos.y, demo.playerPos.z);
ImGui::End();
}
// Sprite test captions, projected with Camera::worldToScreen.
ImDrawList* dl = ImGui::GetForegroundDrawList();
const glm::vec2 viewport(float(window.width()), float(window.height()));
for (const DemoScene::Label& l : demo.labels) {
glm::vec2 px;
if (!cam.worldToScreen(l.pos, viewport, px)) continue;
ImVec2 ts = ImGui::CalcTextSize(l.text);
dl->AddText(ImVec2(px.x - ts.x * 0.5f, px.y - ts.y), IM_COL32(255, 255, 255, 255), l.text);
}
if (showcase.active) showcase.draw(viewport, cam, demo.playerPos, dt);
ui.endFrame();
}
@@ -206,7 +374,7 @@ int run(hd2d::Window& window, const Options& opt) {
if (!capture && window.keyPressed(GLFW_KEY_F12))
hd2d::saveScreenshot(timestampedScreenshotPath(), window.width(), window.height());
if (capture && frame >= opt.frames) {
hd2d::saveScreenshot(opt.screenshot, window.width(), window.height());
hd2d::saveScreenshot(opt.screenshot, window.width(), window.height(), renderer.outputFramebuffer());
HD2D_LOG("player at %.2f %.2f %.2f, GPU %.2f ms", demo.playerPos.x, demo.playerPos.y, demo.playerPos.z,
renderer.gpuFrameMs());
break;
@@ -222,8 +390,10 @@ int run(hd2d::Window& window, const Options& opt) {
int main(int argc, char** argv) {
Options opt = parseArgs(argc, argv);
hd2d::Window window;
// Captures use a fixed-size (floating) window so screenshots are reproducible.
if (!window.create(opt.width, opt.height, "HD-2D Engine Demo", true, opt.screenshot.empty())) return 1;
// Captures use a fixed-size (floating) window so screenshots are reproducible,
// hidden unless --show so they never pop up or take the user's input.
const bool capture = !opt.screenshot.empty();
if (!window.create(opt.width, opt.height, "HD-2D Engine Demo", true, !capture, !capture || opt.show)) return 1;
int rc = run(window, opt);
window.destroy();
return rc;