231 lines
9.5 KiB
C++
231 lines
9.5 KiB
C++
// 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 <GLFW/glfw3.h>
|
|
#include <imgui.h>
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#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<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;
|
|
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<float>(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;
|
|
}
|