// 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. // // Headless-ish capture for testing: // hd2d_demo --screenshot out.png [--frames 120] [--move x,y] [--pos x,z] // [--sun azimuthDeg,elevationDeg] [--yaw deg] [--dist units] [--size WxH] [--ui] // [--set name=value ...] e.g. --set godRays=4 --set dof=0 #include "DemoScene.h" #include "hd2d/DebugUI.h" #include "hd2d/Log.h" #include "hd2d/Renderer.h" #include "hd2d/Screenshot.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; std::vector> settings; // --set name=value glm::vec2 pos{0.0f}, sun{0.0f}; float yaw = 0.0f, dist = 0.0f, rain = 0.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], "--rain")) o.rain = 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; } // 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; for (const auto& [name, value] : opt.settings) if (!applySetting(renderer.settings, name, value)) HD2D_ERR("unknown setting '%s'", name.c_str()); DemoScene demo; 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); } if (opt.rain > 0.0f) { demo.weather.setRain(opt.rain); demo.weather.snap(); // captures start already raining } hd2d::DebugUI ui; if (!ui.init(window)) return 1; bool showUI = capture ? opt.ui : true; hd2d::Camera cam; if (opt.hasYaw) cam.yaw = glm::radians(opt.yaw); 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()); glm::vec2 move = opt.move; if (!ui.wantsKeyboard()) { 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_ESCAPE)) window.requestClose(); } if (!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)); demo.weather.update(dt, demo.scene, renderer.settings); renderer.render(demo.scene, cam, time); if (showUI) { 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(); 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()); 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. if (!window.create(opt.width, opt.height, "HD-2D Engine Demo", true, opt.screenshot.empty())) return 1; int rc = run(window, opt); window.destroy(); return rc; }