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:
@@ -7,17 +7,29 @@
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
// One upright pixel-art sprite. `pos` is where the lowest opaque pixel row
|
||||
// touches the ground; sprites always face the camera around the Y axis.
|
||||
// One upright pixel-art sprite. `pos` is where the feet (pivot) row touches
|
||||
// the ground; sprites face the camera around the Y axis. 64 bytes, read by
|
||||
// billboard.vert as four vec4 attributes (keep both in sync).
|
||||
//
|
||||
// A default-initialised instance draws the whole texture, planted by its
|
||||
// lowest opaque row. Set `uv` to draw one frame of a sprite sheet / atlas
|
||||
// cell (see Texture::cellUV, SpriteAtlas::uv) and `anchor` to that frame's
|
||||
// feet row (Texture::cellAnchor, SpriteAtlas::anchor).
|
||||
struct BillboardInstance {
|
||||
glm::vec3 pos{0.0f};
|
||||
float sway = 0.0f; // wind / player-push response (0 = rigid)
|
||||
glm::vec2 size{1.0f}; // world units of the full texture quad
|
||||
float flip = 0.0f; // 1 = mirror horizontally
|
||||
float tint = 1.0f; // brightness multiplier (breaks up repetition)
|
||||
float sway = 0.0f; // wind / player-push response (0 = rigid)
|
||||
glm::vec2 size{1.0f}; // world size of the `uv` sub-rect quad
|
||||
float flip = 0.0f; // 1 = mirror horizontally (inside the sub-rect)
|
||||
float tint = 1.0f; // brightness multiplier (breaks up repetition)
|
||||
glm::vec4 uv{0, 0, 1, 1}; // texture sub-rect (minU, minV, maxU, maxV), GL orientation (v = 0 is the image bottom)
|
||||
float anchor = -1.0f; // texture-space v of the feet (pivot) row; < 0 = use tex().opaqueBounds().y
|
||||
float turn = 0.0f; // extra yaw (radians) away from camera-facing; color pass only, shadow cards stay sun-facing
|
||||
float alpha = 1.0f; // 0..1 fade, dithered discard in color and shadow passes (no blending)
|
||||
float flash = 0.0f; // 0..1 blend toward bright emissive white (hit / capture flash)
|
||||
};
|
||||
|
||||
// Instanced batch of billboards sharing one texture.
|
||||
// Instanced batch of billboards sharing one texture: its own `texture`, or
|
||||
// `sharedTexture` (e.g. a SpriteAtlas page shared by many batches) when set.
|
||||
class BillboardBatch {
|
||||
public:
|
||||
BillboardBatch() = default;
|
||||
@@ -29,12 +41,20 @@ public:
|
||||
void upload(); // call after editing `instances`
|
||||
void draw() const;
|
||||
|
||||
// World size that keeps the texture at `pixelsPerUnit` texel density.
|
||||
// Texture the renderer binds: `sharedTexture` if set, else `texture`.
|
||||
const Texture& tex() const { return sharedTexture ? *sharedTexture : texture; }
|
||||
|
||||
// World size that keeps the own `texture` at `pixelsPerUnit` texel density.
|
||||
glm::vec2 sizeForDensity(float pixelsPerUnit) const {
|
||||
return glm::vec2(texture.size()) / pixelsPerUnit;
|
||||
}
|
||||
// World size of one `cellPixels` sheet frame / atlas cell at `pixelsPerUnit`.
|
||||
static glm::vec2 cellSize(glm::ivec2 cellPixels, float pixelsPerUnit) {
|
||||
return glm::vec2(cellPixels) / pixelsPerUnit;
|
||||
}
|
||||
|
||||
Texture texture;
|
||||
const Texture* sharedTexture = nullptr; // not owned; must outlive the batch's use
|
||||
std::vector<BillboardInstance> instances;
|
||||
bool castShadows = true;
|
||||
bool inReflections = true;
|
||||
|
||||
@@ -19,6 +19,10 @@ struct Camera {
|
||||
glm::vec3 right() const; // horizontal right vector
|
||||
glm::mat4 view() const;
|
||||
glm::mat4 projection(float aspect) const;
|
||||
// Projects `world` to window pixels (top-left origin) of a `viewportPx`
|
||||
// viewport, using view() and projection(viewportPx.x / viewportPx.y).
|
||||
// Returns false (outPx untouched) when the point is behind the camera.
|
||||
bool worldToScreen(const glm::vec3& world, glm::vec2 viewportPx, glm::vec2& outPx) const;
|
||||
};
|
||||
|
||||
} // namespace hd2d
|
||||
|
||||
44
engine/include/hd2d/DayCycle.h
Normal file
44
engine/include/hd2d/DayCycle.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
#include "hd2d/Weather.h"
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
// Time of day: turns an hour into a complete clear-weather WeatherLook by
|
||||
// smoothly interpolating tweakable keyframes, and moves the directional
|
||||
// light along a sun arc by day and a moon arc by night (so shadows always
|
||||
// work). Feed the result to Weather::setClearLook() every frame; the hour is
|
||||
// usually driven externally (e.g. by a game server).
|
||||
class DayCycle {
|
||||
public:
|
||||
struct Keyframe {
|
||||
float hour; // [0, 24)
|
||||
const char* name; // for tuning UIs
|
||||
WeatherLook look; // sunAzimuth / sunElevation / daylight are ignored (computed from the hour)
|
||||
};
|
||||
|
||||
DayCycle(); // default keys: late night, dawn, morning, noon, afternoon, dusk, night
|
||||
|
||||
WeatherLook look(float hour) const; // any hour, wraps to [0, 24)
|
||||
float daylight(float hour) const; // 0 night .. 1 day (sun height above the horizon)
|
||||
float night(float hour) const; // 0 day .. 1 night (stars + moon visibility)
|
||||
// Direction of the directional light: x = azimuth, y = elevation
|
||||
// (radians, see Sun); the sun between sunrise and sunset, else the moon.
|
||||
glm::vec2 lightAngles(float hour) const;
|
||||
bool moonUp(float hour) const;
|
||||
|
||||
std::vector<Keyframe> keys; // sorted by hour; interpolation wraps around midnight
|
||||
float sunrise = 5.0f, sunset = 19.0f;
|
||||
float sunAzimuth = -0.6f; // azimuth at solar noon
|
||||
float sunSweep = 2.6f; // azimuth travelled from sunrise to sunset
|
||||
float sunMaxElevation = 1.05f;
|
||||
float moonAzimuth = 0.45f; // azimuth at the middle of the night
|
||||
float moonSweep = 1.6f;
|
||||
float moonMaxElevation = 0.62f;
|
||||
float horizonFade = 0.12f; // light elevation (radians) over which it fades in
|
||||
float minElevation = 0.05f; // light never goes lower (keeps shadows stable)
|
||||
};
|
||||
|
||||
} // namespace hd2d
|
||||
@@ -6,6 +6,7 @@ class Window;
|
||||
struct RenderSettings;
|
||||
struct Scene;
|
||||
class Weather;
|
||||
class DayCycle;
|
||||
|
||||
// Dear ImGui integration plus a ready-made tuning panel for the renderer.
|
||||
class DebugUI {
|
||||
@@ -19,8 +20,9 @@ public:
|
||||
|
||||
// Tweak panel for every look parameter (sun, sky, fog, AO, water, post).
|
||||
static void renderSettingsPanel(RenderSettings& settings, Scene& scene, float gpuMs);
|
||||
// Rain amount, transition time and the storm preset.
|
||||
static void weatherPanel(Weather& weather);
|
||||
// Rain / overcast / fog / lightning, transition time and the weather
|
||||
// presets; with `hour` an hour slider, with `day` its keyframes.
|
||||
static void weatherPanel(Weather& weather, DayCycle* day = nullptr, float* hour = nullptr);
|
||||
|
||||
private:
|
||||
bool initialized_ = false;
|
||||
|
||||
@@ -54,7 +54,8 @@ public:
|
||||
glm::vec3 fireLightColor{4.0f, 1.75f, 0.55f};
|
||||
float fireLightRadius = 7.0f;
|
||||
glm::vec3 wind{0.0f}; // world-space drift applied to smoke and embers
|
||||
float ambientFade = 1.0f; // scales fireflies + motes (Weather lowers it in rain)
|
||||
float ambientFade = 1.0f; // scales fireflies + motes (Weather lowers it in rain / mist)
|
||||
float fireflyGlow = 1.0f; // firefly brightness + light (WeatherLook::fireflyGlow, raised at night)
|
||||
|
||||
private:
|
||||
struct Particle {
|
||||
|
||||
@@ -88,6 +88,15 @@ public:
|
||||
bool reloadShaders();
|
||||
void render(const Scene& scene, const Camera& camera, float time);
|
||||
|
||||
// Framebuffer the final tonemap pass writes to (0 = the window's
|
||||
// backbuffer). render() leaves it bound with a full-size viewport, so UI
|
||||
// drawn afterwards (DebugUI, ImGui) lands in it too. It must be at least
|
||||
// the size given to init()/resize() and use a non-sRGB RGBA8 color
|
||||
// attachment like the default framebuffer (the tonemap shader
|
||||
// gamma-encodes itself); OffscreenTarget in Screenshot.h does this.
|
||||
void setOutputFramebuffer(GLuint fbo) { outputFbo_ = fbo; }
|
||||
GLuint outputFramebuffer() const { return outputFbo_; }
|
||||
|
||||
RenderSettings settings;
|
||||
float gpuFrameMs() const { return gpuMs_; }
|
||||
|
||||
@@ -95,6 +104,7 @@ private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
float gpuMs_ = 0.0f;
|
||||
GLuint outputFbo_ = 0;
|
||||
};
|
||||
|
||||
} // namespace hd2d
|
||||
|
||||
@@ -31,6 +31,8 @@ struct Sky {
|
||||
glm::vec3 ground{0.20f, 0.24f, 0.14f}; // bounce light from below
|
||||
float ambient = 0.55f;
|
||||
float fill = 0.35f; // light from the camera side onto faces turned toward the viewer
|
||||
float night = 0.0f; // 0..1: star field + moon disc at the sun direction (the sun acts as the moon)
|
||||
float clouds = 0.0f; // 0..1: extra cloud cover (overcast); rain also covers the sky
|
||||
};
|
||||
|
||||
// Everything the renderer draws. Pointers are non-owning; null entries skip.
|
||||
|
||||
@@ -1,9 +1,36 @@
|
||||
#pragma once
|
||||
#include "hd2d/GL.h"
|
||||
#include "hd2d/RenderTarget.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
// Reads the current backbuffer (before swap) and writes it as PNG.
|
||||
bool saveScreenshot(const std::string& path, int width, int height);
|
||||
// Writes color attachment 0 of `fbo` as PNG; `fbo` = 0 reads the window's
|
||||
// backbuffer (call before swapBuffers).
|
||||
bool saveScreenshot(const std::string& path, int width, int height, GLuint fbo = 0);
|
||||
|
||||
// Window-independent render output for headless captures. Color is plain
|
||||
// RGBA8 (not sRGB) plus a depth/stencil buffer, matching the default
|
||||
// framebuffer, so captures are pixel-identical to on-screen frames.
|
||||
//
|
||||
// hd2d::Window window; window.create(w, h, "capture", false, false, /*visible*/ false);
|
||||
// hd2d::OffscreenTarget out; out.create(window.width(), window.height());
|
||||
// renderer.setOutputFramebuffer(out.fbo());
|
||||
// renderer.render(scene, cam, time); // + any ImGui / UI drawing
|
||||
// out.save("shot.png");
|
||||
class OffscreenTarget {
|
||||
public:
|
||||
bool create(int width, int height);
|
||||
void resize(int width, int height); // reallocates only when the size changes
|
||||
GLuint fbo() const { return target_.fbo(); }
|
||||
GLuint color() const { return target_.color(); }
|
||||
int width() const { return target_.width(); }
|
||||
int height() const { return target_.height(); }
|
||||
bool save(const std::string& path) const { return saveScreenshot(path, width(), height(), fbo()); }
|
||||
|
||||
private:
|
||||
RenderTarget target_;
|
||||
};
|
||||
|
||||
} // namespace hd2d
|
||||
|
||||
56
engine/include/hd2d/SpriteAtlas.h
Normal file
56
engine/include/hd2d/SpriteAtlas.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
#include "hd2d/Texture.h"
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
// Fixed-cell dynamic atlas on one Texture: images of up to `cellPixels` are
|
||||
// packed into a cols x rows grid at runtime (e.g. sprites generated or
|
||||
// downloaded while the game runs) so many billboards share a texture. Use
|
||||
// with `BillboardBatch::sharedTexture = &atlas.texture()` and per-instance
|
||||
// `uv = atlas.uv(slot)`, `anchor = atlas.anchor(slot)`.
|
||||
//
|
||||
// Every cell keeps its own 1-px transparent border (gutter), so sharp
|
||||
// bilinear sampling and the first mip levels never bleed between cells.
|
||||
// Images are placed bottom-centered in their cell; images larger than the
|
||||
// cell are cropped to it (keeping the bottom rows and the horizontal center).
|
||||
class SpriteAtlas {
|
||||
public:
|
||||
// Allocates a transparent texture of cols x rows cells. Clears all slots.
|
||||
bool init(glm::ivec2 cellPixels, int cols, int rows);
|
||||
|
||||
int add(const Image& img); // first free slot, or -1 when full
|
||||
void set(int slot, const Image& img); // replace (or fill) a slot
|
||||
void remove(int slot); // clear a slot and free it
|
||||
|
||||
// BillboardInstance::uv of the whole cell (cellPixels, gutter excluded).
|
||||
glm::vec4 uv(int slot) const;
|
||||
// BillboardInstance::anchor: v of the lowest opaque (alpha >= 128) row of
|
||||
// the image in `slot`; the cell bottom if it has no opaque pixels.
|
||||
float anchor(int slot) const;
|
||||
glm::ivec2 imageSize(int slot) const; // size placed in the cell (after cropping)
|
||||
bool inUse(int slot) const;
|
||||
|
||||
const Texture& texture() const { return texture_; }
|
||||
glm::ivec2 cellPixels() const { return cell_; }
|
||||
int capacity() const { return cols_ * rows_; }
|
||||
int used() const { return used_; }
|
||||
|
||||
private:
|
||||
struct Slot {
|
||||
bool used = false;
|
||||
glm::ivec2 size{0};
|
||||
int feetRow = 0; // lowest opaque row, in cell pixels from the cell bottom
|
||||
};
|
||||
glm::ivec2 origin(int slot) const; // GL-orientation pixel of the cell's bottom-left (inside the gutter)
|
||||
bool valid(int slot) const { return slot >= 0 && slot < capacity(); }
|
||||
|
||||
Texture texture_;
|
||||
glm::ivec2 cell_{0};
|
||||
int cols_ = 0, rows_ = 0, used_ = 0;
|
||||
std::vector<Slot> slots_;
|
||||
};
|
||||
|
||||
} // namespace hd2d
|
||||
@@ -19,9 +19,14 @@ bool loadImage(const std::string& path, Image& out);
|
||||
// Decodes PNG bytes from memory. glTF images use flipY = false (their UV
|
||||
// origin is top-left, which matches GL when rows are uploaded unflipped).
|
||||
bool decodeImage(const unsigned char* data, size_t size, Image& out, bool flipY);
|
||||
// Replaces the colors of fully transparent pixels with their nearest opaque
|
||||
// neighbour (loadImage/decodeImage already do this). Call it on images you
|
||||
// compose yourself so filtering never bleeds dark fringes into edges.
|
||||
void bleedTransparentColors(Image& img);
|
||||
|
||||
// sRGB pixel-art texture. Sampled with "sharp bilinear" in shaders, so it uses
|
||||
// linear filtering with mipmaps rather than GL_NEAREST.
|
||||
// linear filtering with mipmaps rather than GL_NEAREST. `nearest` = GL_NEAREST
|
||||
// without mips instead (crisp ImGui previews of pixel art).
|
||||
class Texture {
|
||||
public:
|
||||
Texture() = default;
|
||||
@@ -31,8 +36,14 @@ public:
|
||||
Texture& operator=(Texture&& o) noexcept;
|
||||
~Texture();
|
||||
|
||||
bool load(const std::string& path, bool repeat = false);
|
||||
bool create(const Image& image, bool repeat = false, float anisotropy = 1.0f);
|
||||
bool load(const std::string& path, bool repeat = false, bool nearest = false);
|
||||
bool create(const Image& image, bool repeat = false, float anisotropy = 1.0f, bool nearest = false);
|
||||
// Fully transparent w x h texture (sRGB RGBA8, mipmapped, clamped) to fill with update().
|
||||
bool createEmpty(int w, int h);
|
||||
// Uploads `img` with its bottom-left pixel at (x, y) (GL orientation,
|
||||
// y = 0 is the bottom row) and regenerates the mips. Does not change
|
||||
// opaqueBounds(), which always describes the image given to create().
|
||||
void update(int x, int y, const Image& img);
|
||||
void bind(GLuint unit) const { glBindTextureUnit(unit, id_); }
|
||||
|
||||
GLuint id() const { return id_; }
|
||||
@@ -41,9 +52,16 @@ public:
|
||||
// Billboards use minY to plant the visible feet on the ground.
|
||||
glm::vec4 opaqueBounds() const { return opaqueBounds_; }
|
||||
|
||||
// Grid sprite sheets authored top-down (row 0 = top row of the PNG), with
|
||||
// `cell` x `cell` pixel frames. cellUV is a BillboardInstance::uv rect;
|
||||
// cellAnchor is a BillboardInstance::anchor: the v of the bottom edge of
|
||||
// pixel row `feetRowFromCellTop` (0 = the cell's top row) of that cell.
|
||||
static glm::vec4 cellUV(glm::ivec2 texSize, glm::ivec2 cell, int col, int rowFromTop);
|
||||
static float cellAnchor(glm::ivec2 texSize, glm::ivec2 cell, int rowFromTop, int feetRowFromCellTop);
|
||||
|
||||
private:
|
||||
GLuint id_ = 0;
|
||||
int width_ = 0, height_ = 0;
|
||||
int width_ = 0, height_ = 0, levels_ = 0;
|
||||
glm::vec4 opaqueBounds_{0, 0, 1, 1};
|
||||
};
|
||||
|
||||
|
||||
162
engine/include/hd2d/UI.h
Normal file
162
engine/include/hd2d/UI.h
Normal file
@@ -0,0 +1,162 @@
|
||||
#pragma once
|
||||
// HD-2D style game UI (Octopath Traveler / Triangle Strategy look) drawn with
|
||||
// Dear ImGui draw lists: translucent navy panels with a gold double border and
|
||||
// corner gems, serif text with a soft shadow, name plates, menus, progress
|
||||
// bars, a typewriter dialogue box with choices, and toast notifications.
|
||||
//
|
||||
// Everything draws into an ImDrawList (usually ImGui::GetForegroundDrawList()),
|
||||
// so no ImGui windows are needed. Sizes are in pixels at a 1080 px tall
|
||||
// viewport; multiply by scaleFor(viewport) (the widgets taking `scale` or a
|
||||
// viewport do this for you).
|
||||
//
|
||||
// Setup: DebugUI::init(), then ui::loadFonts(dir, ui::scaleFor(viewport))
|
||||
// before the first ImGui frame (or between frames to rebake after a resize).
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace hd2d::ui {
|
||||
|
||||
// Game fonts. `body` dialogue/menu text (serif), `title` large display face,
|
||||
// `name` speaker / name plates (small caps serif), `small` captions and toasts.
|
||||
// ImGui's default font stays the default so debug panels are unaffected.
|
||||
struct Fonts {
|
||||
ImFont* body = nullptr;
|
||||
ImFont* title = nullptr;
|
||||
ImFont* name = nullptr;
|
||||
ImFont* small = nullptr;
|
||||
};
|
||||
|
||||
// Nominal font sizes (px at 1080p). Fonts are baked at size * pixelScale so
|
||||
// text drawn at size * scaleFor(viewport) is pixel-exact.
|
||||
inline constexpr float kBodySize = 34.0f;
|
||||
inline constexpr float kTitleSize = 96.0f;
|
||||
inline constexpr float kNameSize = 28.0f;
|
||||
inline constexpr float kSmallSize = 26.0f;
|
||||
|
||||
// Loads the fonts from `fontDir` (assets/fonts) baked for `pixelScale` and
|
||||
// rebuilds the ImGui font atlas. Requires an ImGui context (DebugUI::init)
|
||||
// and must be called outside an ImGui frame. Returns false if a font file is
|
||||
// missing (the missing slots fall back to the default font).
|
||||
bool loadFonts(const std::string& fontDir, float pixelScale = 1.0f);
|
||||
const Fonts& fonts();
|
||||
|
||||
// Styles standard ImGui widgets (windows, buttons, input text, sliders, combo,
|
||||
// checkboxes, tabs, scrollbars) in the same navy + gold palette.
|
||||
void applyTheme();
|
||||
|
||||
// UI scale for a viewport in pixels: 1.0 at 1080 px tall.
|
||||
float scaleFor(ImVec2 viewport);
|
||||
|
||||
// Shared palette.
|
||||
inline constexpr ImU32 kGold = IM_COL32(222, 189, 120, 255);
|
||||
inline constexpr ImU32 kGoldDim = IM_COL32(150, 122, 74, 255);
|
||||
inline constexpr ImU32 kTextColor = IM_COL32(246, 240, 226, 255);
|
||||
inline constexpr ImU32 kTextMuted = IM_COL32(184, 178, 164, 255);
|
||||
|
||||
// `alpha` is the background opacity; `tint` modulates the border/ornament
|
||||
// colour and its alpha fades the whole panel (shadow, background, border).
|
||||
// `rounding` is in 1080p pixels; `scale` 0 = scaleFor(ImGui display size).
|
||||
struct PanelStyle {
|
||||
float alpha = 0.88f;
|
||||
bool ornate = true;
|
||||
float rounding = 6.0f;
|
||||
ImU32 tint = IM_COL32(255, 255, 255, 255);
|
||||
float scale = 0.0f;
|
||||
};
|
||||
void panel(ImDrawList* dl, ImVec2 min, ImVec2 max, const PanelStyle& style = {});
|
||||
|
||||
// Text with a 1px dark drop shadow. `font` may be null (current ImGui font).
|
||||
void text(ImDrawList* dl, ImFont* font, float size, ImVec2 pos, ImU32 color, std::string_view s, bool shadow = true);
|
||||
// Word-wrapped variant of text(); wrapWidth <= 0 disables wrapping.
|
||||
void textWrapped(ImDrawList* dl, ImFont* font, float size, ImVec2 pos, ImU32 color, std::string_view s,
|
||||
float wrapWidth, bool shadow = true);
|
||||
ImVec2 measure(ImFont* font, float size, std::string_view s, float wrapWidth = 0.0f);
|
||||
void textCentered(ImDrawList* dl, ImFont* font, float size, ImVec2 center, ImU32 color, std::string_view s,
|
||||
bool shadow = true);
|
||||
|
||||
// Big glowing gold title with flourish rules underneath (title screens).
|
||||
void titleText(ImDrawList* dl, ImVec2 center, std::string_view s, float size);
|
||||
|
||||
// Small pill label whose pointer tip sits on `anchorPx` (e.g. a projected
|
||||
// head position). `accent` colours the border and pointer.
|
||||
void nameplate(ImDrawList* dl, ImVec2 anchorPx, std::string_view s, ImU32 accent, float scale = 1.0f);
|
||||
|
||||
// Vertical option list in a panel with an animated cursor on `selected`.
|
||||
void menu(ImDrawList* dl, ImVec2 center, const std::vector<std::string>& items, int selected,
|
||||
std::string_view title = {}, float scale = 1.0f);
|
||||
// Same as menu(), placed so that `pivot` (0..1 of the panel size) lands on `pos`.
|
||||
void menuAt(ImDrawList* dl, ImVec2 pos, ImVec2 pivot, const std::vector<std::string>& items, int selected,
|
||||
std::string_view title = {}, float scale = 1.0f);
|
||||
// Panel size menu()/menuAt() will use.
|
||||
ImVec2 menuSize(const std::vector<std::string>& items, std::string_view title = {}, float scale = 1.0f);
|
||||
|
||||
// Recessed bar filled to t (0..1) with a vertical gradient of `color`.
|
||||
void progressBar(ImDrawList* dl, ImVec2 min, ImVec2 max, float t, ImU32 color);
|
||||
|
||||
// Small blinking/bobbing gem pointing down ("more text" indicator); `time` drives the animation.
|
||||
void advanceIndicator(ImDrawList* dl, ImVec2 center, float scale, float time);
|
||||
|
||||
struct DialogueLine {
|
||||
std::string speaker; // empty = no name plate
|
||||
std::string text; // UTF-8, word-wrapped to the box
|
||||
ImTextureID portrait = 0; // optional, drawn square on the left inside the box
|
||||
ImVec2 uv0{0, 0}, uv1{1, 1};
|
||||
};
|
||||
|
||||
// Bottom-centre typewriter dialogue box. Typical loop:
|
||||
// if (confirmPressed && box.advance()) handle(box.choice());
|
||||
// box.update(dt); box.draw(viewport);
|
||||
class DialogueBox {
|
||||
public:
|
||||
void start(std::vector<DialogueLine> lines, std::vector<std::string> choices = {}); // choices shown with the last line
|
||||
bool isOpen() const { return open_; }
|
||||
void update(float dt);
|
||||
bool advance(); // reveal the rest of the line, else next line; returns true when the conversation closed
|
||||
void moveChoice(int delta);
|
||||
int choice() const { return chosen_; } // chosen index once closed with choices, else -1
|
||||
void close();
|
||||
void draw(ImVec2 viewport); // draws into ImGui::GetForegroundDrawList()
|
||||
|
||||
bool lineRevealed() const; // current line fully shown
|
||||
bool choicesVisible() const; // last line revealed and choices pending
|
||||
|
||||
float charsPerSecond = 48.0f;
|
||||
float sentencePause = 0.22f; // extra seconds after . ! ? and an ellipsis
|
||||
float clausePause = 0.09f; // extra seconds after , ; : and dashes
|
||||
|
||||
private:
|
||||
void stepChar();
|
||||
|
||||
std::vector<DialogueLine> lines_;
|
||||
std::vector<std::string> choices_;
|
||||
size_t line_ = 0;
|
||||
size_t revealed_ = 0; // bytes of the current line shown (always on a UTF-8 boundary)
|
||||
float budget_ = 0.0f; // seconds banked for the typewriter
|
||||
float time_ = 0.0f;
|
||||
int selected_ = 0;
|
||||
int chosen_ = -1;
|
||||
bool open_ = false;
|
||||
};
|
||||
|
||||
// Stacked notification banners at the top centre that slide in and fade out.
|
||||
class Toasts {
|
||||
public:
|
||||
void push(std::string text, ImU32 accent = IM_COL32(230, 190, 90, 255), float seconds = 3.5f);
|
||||
void update(float dt);
|
||||
void draw(ImVec2 viewport); // draws into ImGui::GetForegroundDrawList()
|
||||
void clear() { items_.clear(); }
|
||||
|
||||
private:
|
||||
struct Item {
|
||||
std::string text;
|
||||
ImU32 accent;
|
||||
float seconds, age = 0.0f;
|
||||
};
|
||||
std::vector<Item> items_;
|
||||
};
|
||||
|
||||
} // namespace hd2d::ui
|
||||
@@ -8,46 +8,87 @@ namespace hd2d {
|
||||
|
||||
struct RenderSettings;
|
||||
|
||||
// Everything the weather changes about the look, so it can be blended.
|
||||
// Everything time of day and weather change about the look, so it can be
|
||||
// blended. One look fully describes the lighting of a frame.
|
||||
struct WeatherLook {
|
||||
// Directional light: the sun by day, the moon at night (shadows follow it).
|
||||
float sunAzimuth = -0.6f, sunElevation = 0.66f; // radians, see Sun
|
||||
float sunIntensity = 0.0f;
|
||||
glm::vec3 sunColor{1.0f};
|
||||
glm::vec3 zenith{0.5f}, horizon{0.5f}, ground{0.2f};
|
||||
float ambient = 0.5f;
|
||||
float ambient = 0.5f, fill = 0.35f;
|
||||
float night = 0.0f; // 0..1: star field + moon disc (Sky::night)
|
||||
float clouds = 0.0f; // 0..1: extra sky cloud cover (Sky::clouds)
|
||||
float daylight = 1.0f; // 0 night .. 1 day: scales weather presets (a storm at night is dark)
|
||||
glm::vec3 fogColor{0.5f};
|
||||
float fogDensity = 0.0f, fogHeightFalloff = 0.1f, godRays = 0.0f, shaftStrength = 0.0f, cookieStrength = 0.0f;
|
||||
float shadowSoftness = 1.5f;
|
||||
float windStrength = 0.0f, windGustSpeed = 0.0f;
|
||||
float bloomStrength = 0.0f, saturation = 1.0f;
|
||||
float bloomStrength = 0.0f, saturation = 1.0f, exposure = 1.0f;
|
||||
glm::vec3 lift{0.0f}; // grading lift (shadow tint)
|
||||
glm::vec3 gain{1.0f}; // grading gain (highlight tint)
|
||||
float fireflyGlow = 1.0f; // ParticleSystem::fireflyGlow (raise at night)
|
||||
};
|
||||
|
||||
// Blends the scene from its clear-weather look (captured from the current
|
||||
// scene + settings while it is not raining, so manual tuning is kept) toward
|
||||
// a storm preset, and drives rain intensity, wetness, ground splashes and the
|
||||
// fading of ambient particles.
|
||||
// Linear blend of every field (t = 0: a, t = 1: b).
|
||||
WeatherLook mixLooks(const WeatherLook& a, const WeatherLook& b, float t);
|
||||
|
||||
// Blends the scene from its clear-weather look toward overcast, fog (mist)
|
||||
// and storm presets, and drives rain intensity, wetness, ground splashes,
|
||||
// lightning and the fading of ambient particles.
|
||||
//
|
||||
// The clear look is either captured from the current scene + settings while
|
||||
// the weather is fully clear (so manual tuning is kept), or supplied every
|
||||
// frame with setClearLook() (e.g. from a DayCycle). Presets are authored at
|
||||
// full daylight and darkened by the clear look's `daylight`.
|
||||
class Weather {
|
||||
public:
|
||||
Weather();
|
||||
|
||||
// Replaces the captured clear look (call every frame, e.g. with
|
||||
// DayCycle::look(hour)). Once called, the scene is no longer captured.
|
||||
void setClearLook(const WeatherLook& look);
|
||||
|
||||
void setRain(float target) { targetRain_ = glm::clamp(target, 0.0f, 1.0f); }
|
||||
void setOvercast(float target) { targetOvercast_ = glm::clamp(target, 0.0f, 1.0f); }
|
||||
void setFog(float target) { targetFog_ = glm::clamp(target, 0.0f, 1.0f); }
|
||||
void setLightning(bool enabled) { lightning_ = enabled; } // occasional flashes while raining
|
||||
float targetRain() const { return targetRain_; }
|
||||
float targetOvercast() const { return targetOvercast_; }
|
||||
float targetFog() const { return targetFog_; }
|
||||
bool lightning() const { return lightning_; }
|
||||
float rain() const { return rain_; }
|
||||
// Jumps straight to the target (no transition), e.g. for screenshots.
|
||||
void snap() { rain_ = targetRain_; wetness_ = targetRain_; }
|
||||
float overcast() const { return overcast_; }
|
||||
float fog() const { return fog_; }
|
||||
// Jumps straight to every target (no transition), e.g. for screenshots.
|
||||
void snap();
|
||||
|
||||
void update(float dt, Scene& scene, RenderSettings& settings);
|
||||
|
||||
WeatherLook storm; // look at full rain intensity
|
||||
float transitionSeconds = 5.0f; // clear <-> storm fade time
|
||||
// Presets at full daylight. Time-of-day fields (sun direction, night,
|
||||
// daylight, exposure, lift, gain, fill, fireflies) always come from the clear look.
|
||||
WeatherLook storm; // full rain intensity
|
||||
WeatherLook overcastLook; // grey sky, weak sun, soft shadows
|
||||
WeatherLook mist; // thick low fog (morning mist)
|
||||
float nightBrightness = 0.3f; // preset brightness at daylight = 0
|
||||
float transitionSeconds = 5.0f; // fade time for every target
|
||||
float splashesPerSecond = 500.0f;
|
||||
float splashRadius = 12.0f; // around Scene::focusPoint
|
||||
float lightningInterval = 7.0f; // mean seconds between flashes at full rain
|
||||
|
||||
// The look currently set on a scene + settings (e.g. a hand-tuned noon).
|
||||
static WeatherLook capture(const Scene& scene, const RenderSettings& settings);
|
||||
|
||||
private:
|
||||
static WeatherLook capture(const Scene& scene, const RenderSettings& settings);
|
||||
static void apply(const WeatherLook& look, Scene& scene, RenderSettings& settings);
|
||||
WeatherLook relative(const WeatherLook& preset, float hueKeep) const;
|
||||
bool clear() const;
|
||||
|
||||
WeatherLook clear_;
|
||||
bool haveClear_ = false;
|
||||
bool haveClear_ = false, externalClear_ = false, lightning_ = false;
|
||||
float rain_ = 0.0f, targetRain_ = 0.0f, wetness_ = 0.0f, splashAccum_ = 0.0f;
|
||||
float overcast_ = 0.0f, targetOvercast_ = 0.0f, fog_ = 0.0f, targetFog_ = 0.0f;
|
||||
float flash_ = 0.0f, flashAge_ = 1e3f, nextFlash_ = 3.0f;
|
||||
std::mt19937 rng_{99};
|
||||
};
|
||||
|
||||
|
||||
@@ -10,19 +10,27 @@ namespace hd2d {
|
||||
class Window {
|
||||
public:
|
||||
// Non-resizable windows float on tiling WMs, keeping an exact size.
|
||||
bool create(int width, int height, const std::string& title, bool vsync = true, bool resizable = true);
|
||||
// `visible = false` creates a hidden, never-focused window that only
|
||||
// provides the GL context (headless captures: render into an
|
||||
// OffscreenTarget, see Screenshot.h). Hidden windows ignore vsync,
|
||||
// swapBuffers() is a no-op (unmapped Wayland surfaces never get frame
|
||||
// callbacks, so a real swap could block forever) and every input query
|
||||
// reports nothing, so the user's keyboard/mouse cannot affect them.
|
||||
bool create(int width, int height, const std::string& title, bool vsync = true, bool resizable = true,
|
||||
bool visible = true);
|
||||
void destroy();
|
||||
|
||||
// Polls events and refreshes per-frame input state. Returns false once the
|
||||
// window should close.
|
||||
bool pollEvents();
|
||||
void swapBuffers();
|
||||
bool visible() const { return visible_; }
|
||||
|
||||
bool keyDown(int glfwKey) const;
|
||||
bool keyPressed(int glfwKey) const; // went down this frame
|
||||
bool mouseDown(int glfwButton) const;
|
||||
glm::vec2 mouseDelta() const { return mouseDelta_; }
|
||||
float scrollDelta() const { return scrollDelta_; }
|
||||
glm::vec2 mouseDelta() const { return visible_ ? mouseDelta_ : glm::vec2(0.0f); }
|
||||
float scrollDelta() const { return visible_ ? scrollDelta_ : 0.0f; }
|
||||
|
||||
int width() const { return width_; }
|
||||
int height() const { return height_; }
|
||||
@@ -38,6 +46,7 @@ private:
|
||||
GLFWwindow* window_ = nullptr;
|
||||
int width_ = 0, height_ = 0;
|
||||
bool resized_ = false;
|
||||
bool visible_ = true;
|
||||
bool keys_[512] = {};
|
||||
bool prevKeys_[512] = {};
|
||||
glm::vec2 lastMouse_{0.0f};
|
||||
|
||||
Reference in New Issue
Block a user