// HD-2D engine demo: meadow diorama with a WASD-controlled sprite. // // Controls: WASD/arrows move, Q/E rotate camera, right-drag orbit, wheel zoom, // 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. // // 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 #include #include #include #include #include #include #include #include #include #include #ifndef HD2D_ASSET_DIR #define HD2D_ASSET_DIR "assets" #endif namespace { struct Options { std::string screenshot; int frames = 120; glm::vec2 move{0.0f}; bool hasPos = false, hasSun = false, hasYaw = false, ui = false, spriteTest = false, uiDemo = false, show = false; bool hasHour = false, hasPitch = false, lightning = false; std::vector> settings; // --set name=value glm::vec2 pos{0.0f}, sun{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; }; bool parsePair(const char* s, float& a, float& b, char sep = ',') { char fmt[16]; std::snprintf(fmt, sizeof fmt, "%%f%c%%f", sep); return std::sscanf(s, fmt, &a, &b) == 2; } Options parseArgs(int argc, char** argv) { Options o; for (int i = 1; i < argc; ++i) { auto next = [&]() -> const char* { return i + 1 < argc ? argv[++i] : ""; }; if (!std::strcmp(argv[i], "--screenshot")) o.screenshot = next(); else if (!std::strcmp(argv[i], "--frames")) o.frames = std::max(1, std::atoi(next())); else if (!std::strcmp(argv[i], "--move")) parsePair(next(), o.move.x, o.move.y); else if (!std::strcmp(argv[i], "--pos")) o.hasPos = parsePair(next(), o.pos.x, o.pos.y); else if (!std::strcmp(argv[i], "--sun")) o.hasSun = parsePair(next(), o.sun.x, o.sun.y); 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('='); if (eq != std::string::npos) o.settings.emplace_back(kv.substr(0, eq), float(std::atof(kv.c_str() + eq + 1))); } else if (!std::strcmp(argv[i], "--size")) { float w, h; if (parsePair(next(), w, h, 'x')) o.width = int(w), o.height = int(h); } else std::fprintf(stderr, "unknown argument: %s\n", argv[i]); } return o; } // Overrides a renderer setting by name (bools take 0/1). Returns false if unknown. bool applySetting(hd2d::RenderSettings& s, const std::string& name, float v) { struct Entry { const char* name; float* f; bool* b; } table[] = { {"volumetrics", nullptr, &s.volumetrics}, {"dof", nullptr, &s.dof}, {"bloom", nullptr, &s.bloom}, {"reflections", nullptr, &s.reflections}, {"fogDensity", &s.fogDensity, nullptr}, {"fogHeightFalloff", &s.fogHeightFalloff, nullptr}, {"godRays", &s.godRayIntensity, nullptr}, {"shaftStrength", &s.shaftStrength, nullptr}, {"shaftCoverage", &s.shaftCoverage, nullptr}, {"cookieStrength", &s.cookieStrength, nullptr}, {"exposure", &s.exposure, nullptr}, {"bloomStrength", &s.bloomStrength, nullptr}, {"tiltShift", &s.tiltShift, nullptr}, {"focusRange", &s.focusRange, nullptr}, {"shaftScale", &s.shaftScale, nullptr}, {"occluderFade", nullptr, &s.occluderFade}, {"ssao", nullptr, &s.ssao}, {"groundAO", nullptr, &s.groundOcclusion}, {"ssaoRadius", &s.ssaoRadius, nullptr}, {"ssaoIntensity", &s.ssaoIntensity, nullptr}, }; for (Entry& e : table) if (name == e.name) { if (e.f) *e.f = v; else *e.b = v != 0.0f; return true; } return false; } std::string timestampedScreenshotPath() { std::time_t t = std::time(nullptr); char buf[64]; std::strftime(buf, sizeof buf, "screenshots/hd2d_%Y%m%d_%H%M%S.png", std::localtime(&t)); 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; } if (opt.hasPos) demo.placePlayer(opt.pos); if (opt.hasSun) { demo.scene.sun.azimuth = glm::radians(opt.sun.x); demo.scene.sun.elevation = glm::radians(opt.sun.y); } // 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); using clock = std::chrono::steady_clock; auto last = clock::now(); float time = 0.0f; int frame = 0; while (window.pollEvents()) { auto now = clock::now(); float dt = std::min(std::chrono::duration(now - last).count(), 0.05f); last = now; if (capture) dt = 1.0f / 60.0f; // deterministic captures time += dt; 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 (!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; if (window.keyDown(GLFW_KEY_A) || window.keyDown(GLFW_KEY_LEFT)) move.x -= 1.0f; if (window.keyDown(GLFW_KEY_Q)) cam.yaw -= dt * 1.2f; if (window.keyDown(GLFW_KEY_E)) cam.yaw += dt * 1.2f; 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 (!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(); cam.yaw += d.x * 0.005f; cam.pitch = std::clamp(cam.pitch + d.y * 0.004f, 0.2f, 1.35f); } } demo.update(dt, time, move, cam); 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 || opt.spriteTest || showcase.active) { ui.beginFrame(); 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(); } ++frame; 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(), renderer.outputFramebuffer()); HD2D_LOG("player at %.2f %.2f %.2f, GPU %.2f ms", demo.playerPos.x, demo.playerPos.y, demo.playerPos.z, renderer.gpuFrameMs()); break; } window.swapBuffers(); } ui.shutdown(); return 0; } } // namespace 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, // 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; }