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

@@ -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;

View File

@@ -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

View 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

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View 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

View File

@@ -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
View 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

View File

@@ -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};
};

View File

@@ -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};