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};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
static_assert(sizeof(BillboardInstance) == 32, "instance layout must match billboard.vert");
|
||||
static_assert(sizeof(BillboardInstance) == 64, "instance layout must match billboard.vert");
|
||||
|
||||
BillboardBatch::~BillboardBatch() {
|
||||
if (vbo_) glDeleteBuffers(1, &vbo_);
|
||||
@@ -16,12 +16,14 @@ void BillboardBatch::upload() {
|
||||
glCreateVertexArrays(1, &vao_);
|
||||
glCreateBuffers(1, &vbo_);
|
||||
glVertexArrayBindingDivisor(vao_, 0, 1);
|
||||
glEnableVertexArrayAttrib(vao_, 0);
|
||||
glVertexArrayAttribFormat(vao_, 0, 4, GL_FLOAT, GL_FALSE, offsetof(BillboardInstance, pos));
|
||||
glVertexArrayAttribBinding(vao_, 0, 0);
|
||||
glEnableVertexArrayAttrib(vao_, 1);
|
||||
glVertexArrayAttribFormat(vao_, 1, 4, GL_FLOAT, GL_FALSE, offsetof(BillboardInstance, size));
|
||||
glVertexArrayAttribBinding(vao_, 1, 0);
|
||||
// Four vec4 attributes: pos+sway, size+flip+tint, uv rect, anchor+turn+alpha+flash.
|
||||
const GLuint offsets[] = {GLuint(offsetof(BillboardInstance, pos)), GLuint(offsetof(BillboardInstance, size)),
|
||||
GLuint(offsetof(BillboardInstance, uv)), GLuint(offsetof(BillboardInstance, anchor))};
|
||||
for (GLuint a = 0; a < 4; ++a) {
|
||||
glEnableVertexArrayAttrib(vao_, a);
|
||||
glVertexArrayAttribFormat(vao_, a, 4, GL_FLOAT, GL_FALSE, offsets[a]);
|
||||
glVertexArrayAttribBinding(vao_, a, 0);
|
||||
}
|
||||
}
|
||||
count_ = GLsizei(instances.size());
|
||||
if (instances.size() > capacity_) {
|
||||
|
||||
@@ -16,4 +16,13 @@ glm::mat4 Camera::view() const { return glm::lookAt(position(), target, glm::vec
|
||||
|
||||
glm::mat4 Camera::projection(float aspect) const { return glm::perspective(fovY, aspect, nearPlane, farPlane); }
|
||||
|
||||
bool Camera::worldToScreen(const glm::vec3& world, glm::vec2 viewportPx, glm::vec2& outPx) const {
|
||||
if (viewportPx.x <= 0.0f || viewportPx.y <= 0.0f) return false;
|
||||
glm::vec4 clip = projection(viewportPx.x / viewportPx.y) * view() * glm::vec4(world, 1.0f);
|
||||
if (clip.w <= 1e-6f) return false;
|
||||
glm::vec2 ndc = glm::vec2(clip) / clip.w;
|
||||
outPx = glm::vec2((ndc.x * 0.5f + 0.5f) * viewportPx.x, (0.5f - ndc.y * 0.5f) * viewportPx.y);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace hd2d
|
||||
|
||||
175
engine/src/DayCycle.cpp
Normal file
175
engine/src/DayCycle.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
#include "hd2d/DayCycle.h"
|
||||
|
||||
#include "hd2d/Renderer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kPi = 3.14159265f;
|
||||
|
||||
float wrapHour(float h) {
|
||||
h = std::fmod(h, 24.0f);
|
||||
return h < 0.0f ? h + 24.0f : h;
|
||||
}
|
||||
|
||||
float smooth(float e0, float e1, float x) {
|
||||
float t = std::clamp((x - e0) / (e1 - e0), 0.0f, 1.0f);
|
||||
return t * t * (3.0f - 2.0f * t);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DayCycle::DayCycle() {
|
||||
// Noon is the engine's default look; the other keys are derived from it.
|
||||
const WeatherLook noon = Weather::capture(Scene{}, RenderSettings{});
|
||||
|
||||
// Night: dim cool moonlight, deep blue sky and shadows, desaturated
|
||||
// greens; warm fires and fireflies carry the scene.
|
||||
WeatherLook night = noon;
|
||||
night.sunColor = {0.34f, 0.50f, 1.0f};
|
||||
night.sunIntensity = 0.45f;
|
||||
night.zenith = {0.010f, 0.022f, 0.075f};
|
||||
night.horizon = {0.030f, 0.060f, 0.180f};
|
||||
night.ground = {0.012f, 0.020f, 0.045f};
|
||||
night.ambient = 0.9f;
|
||||
night.fill = 0.2f;
|
||||
night.night = 1.0f;
|
||||
night.fogColor = {0.025f, 0.045f, 0.11f};
|
||||
night.fogDensity = 0.01f;
|
||||
night.godRays = 0.5f;
|
||||
night.shaftStrength = 0.7f;
|
||||
night.cookieStrength = 0.35f;
|
||||
night.bloomStrength = 0.12f;
|
||||
night.saturation = 0.75f;
|
||||
night.exposure = 1.2f;
|
||||
night.lift = {0.004f, 0.012f, 0.045f};
|
||||
night.gain = {0.94f, 0.97f, 1.08f};
|
||||
night.fireflyGlow = 1.5f;
|
||||
|
||||
// Dawn: low warm pink sun, lilac sky, rosy haze.
|
||||
WeatherLook dawn = noon;
|
||||
dawn.sunColor = {1.0f, 0.62f, 0.45f};
|
||||
dawn.sunIntensity = 2.6f;
|
||||
dawn.zenith = {0.30f, 0.36f, 0.62f};
|
||||
dawn.horizon = {1.0f, 0.68f, 0.66f};
|
||||
dawn.ground = {0.18f, 0.17f, 0.17f};
|
||||
dawn.ambient = 0.6f;
|
||||
dawn.night = 0.12f;
|
||||
dawn.fogColor = {0.55f, 0.42f, 0.50f};
|
||||
dawn.fogDensity = 0.012f;
|
||||
dawn.godRays = 3.5f;
|
||||
dawn.bloomStrength = 0.09f;
|
||||
dawn.saturation = 1.0f;
|
||||
dawn.exposure = 1.1f;
|
||||
dawn.lift = {0.025f, 0.015f, 0.035f};
|
||||
dawn.gain = {1.06f, 0.96f, 0.98f};
|
||||
dawn.fireflyGlow = 1.4f;
|
||||
|
||||
// Morning: fresh, slightly cool, crisp.
|
||||
WeatherLook morning = noon;
|
||||
morning.sunColor = {1.0f, 0.90f, 0.76f};
|
||||
morning.sunIntensity = 2.9f;
|
||||
morning.zenith = {0.24f, 0.44f, 0.80f};
|
||||
morning.horizon = {0.74f, 0.79f, 0.82f};
|
||||
morning.fogColor = {0.32f, 0.38f, 0.47f};
|
||||
morning.fogDensity = 0.01f;
|
||||
morning.godRays = 5.0f;
|
||||
morning.fireflyGlow = 0.8f;
|
||||
|
||||
// Afternoon: warmer, golden.
|
||||
WeatherLook afternoon = noon;
|
||||
afternoon.sunColor = {1.0f, 0.80f, 0.56f};
|
||||
afternoon.sunIntensity = 3.0f;
|
||||
afternoon.zenith = {0.22f, 0.40f, 0.74f};
|
||||
afternoon.horizon = {0.80f, 0.76f, 0.68f};
|
||||
afternoon.ground = {0.22f, 0.23f, 0.13f};
|
||||
afternoon.fogColor = {0.36f, 0.36f, 0.40f};
|
||||
afternoon.saturation = 1.14f;
|
||||
afternoon.lift = {0.016f, 0.010f, 0.022f};
|
||||
|
||||
// Dusk: deep orange sun under a purple sky, violet shadows.
|
||||
WeatherLook dusk = noon;
|
||||
dusk.sunColor = {1.0f, 0.45f, 0.20f};
|
||||
dusk.sunIntensity = 3.0f;
|
||||
dusk.zenith = {0.18f, 0.14f, 0.38f};
|
||||
dusk.horizon = {0.95f, 0.45f, 0.35f};
|
||||
dusk.ground = {0.16f, 0.10f, 0.14f};
|
||||
dusk.ambient = 0.65f;
|
||||
dusk.fill = 0.3f;
|
||||
dusk.night = 0.1f;
|
||||
dusk.fogColor = {0.35f, 0.20f, 0.30f};
|
||||
dusk.fogDensity = 0.008f;
|
||||
dusk.godRays = 2.2f;
|
||||
dusk.bloomStrength = 0.1f;
|
||||
dusk.saturation = 1.1f;
|
||||
dusk.exposure = 1.0f;
|
||||
dusk.lift = {0.030f, 0.010f, 0.045f};
|
||||
dusk.gain = {1.06f, 0.95f, 0.92f};
|
||||
dusk.fireflyGlow = 1.6f;
|
||||
|
||||
keys = {
|
||||
{3.5f, "late night", night}, {6.0f, "dawn", dawn}, {9.0f, "morning", morning}, {12.5f, "noon", noon},
|
||||
{15.5f, "afternoon", afternoon}, {18.0f, "dusk", dusk}, {20.5f, "night", night},
|
||||
};
|
||||
}
|
||||
|
||||
bool DayCycle::moonUp(float hour) const {
|
||||
float h = wrapHour(hour);
|
||||
return h < sunrise || h >= sunset;
|
||||
}
|
||||
|
||||
// Raw (unclamped) azimuth / elevation of the active light.
|
||||
static glm::vec2 rawAngles(const DayCycle& d, float h) {
|
||||
if (!d.moonUp(h)) {
|
||||
float t = (h - d.sunrise) / std::max(d.sunset - d.sunrise, 1e-3f);
|
||||
return {d.sunAzimuth + (t - 0.5f) * d.sunSweep, d.sunMaxElevation * std::sin(kPi * t)};
|
||||
}
|
||||
float len = std::max(24.0f - (d.sunset - d.sunrise), 1e-3f);
|
||||
float t = wrapHour(h - d.sunset) / len;
|
||||
return {d.moonAzimuth + (t - 0.5f) * d.moonSweep, d.moonMaxElevation * std::sin(kPi * t)};
|
||||
}
|
||||
|
||||
glm::vec2 DayCycle::lightAngles(float hour) const {
|
||||
glm::vec2 a = rawAngles(*this, wrapHour(hour));
|
||||
a.y = std::max(a.y, minElevation);
|
||||
return a;
|
||||
}
|
||||
|
||||
float DayCycle::daylight(float hour) const {
|
||||
float h = wrapHour(hour);
|
||||
if (moonUp(h)) return 0.0f;
|
||||
return smooth(0.0f, 0.4f, rawAngles(*this, h).y);
|
||||
}
|
||||
|
||||
static WeatherLook interpolate(const std::vector<DayCycle::Keyframe>& keys, float h) {
|
||||
if (keys.empty()) return {};
|
||||
if (keys.size() == 1) return keys[0].look;
|
||||
// Last key at or before h (wrapping to the final key before the first).
|
||||
size_t i = keys.size() - 1;
|
||||
for (size_t k = 0; k < keys.size(); ++k)
|
||||
if (keys[k].hour <= h) i = k;
|
||||
size_t j = (i + 1) % keys.size();
|
||||
float span = wrapHour(keys[j].hour - keys[i].hour);
|
||||
float t = span > 1e-4f ? wrapHour(h - keys[i].hour) / span : 0.0f;
|
||||
t = std::clamp(t, 0.0f, 1.0f);
|
||||
return mixLooks(keys[i].look, keys[j].look, t * t * (3.0f - 2.0f * t));
|
||||
}
|
||||
|
||||
float DayCycle::night(float hour) const { return interpolate(keys, wrapHour(hour)).night; }
|
||||
|
||||
WeatherLook DayCycle::look(float hour) const {
|
||||
float h = wrapHour(hour);
|
||||
WeatherLook l = interpolate(keys, h);
|
||||
glm::vec2 raw = rawAngles(*this, h);
|
||||
l.sunAzimuth = raw.x;
|
||||
l.sunElevation = std::max(raw.y, minElevation);
|
||||
l.sunIntensity *= smooth(0.0f, horizonFade, raw.y); // light sinks below the horizon
|
||||
l.daylight = daylight(h);
|
||||
return l;
|
||||
}
|
||||
|
||||
} // namespace hd2d
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "hd2d/DebugUI.h"
|
||||
|
||||
#include "hd2d/DayCycle.h"
|
||||
#include "hd2d/Renderer.h"
|
||||
#include "hd2d/Weather.h"
|
||||
#include "hd2d/Window.h"
|
||||
@@ -138,24 +139,77 @@ void DebugUI::renderSettingsPanel(RenderSettings& s, Scene& scene, float gpuMs)
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void DebugUI::weatherPanel(Weather& w) {
|
||||
void DebugUI::weatherPanel(Weather& w, DayCycle* day, float* hour) {
|
||||
ImGui::SetNextWindowPos(ImVec2(364, 12), ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin("Weather", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
|
||||
if (hour) {
|
||||
ImGui::SliderFloat("Hour", hour, 0.0f, 24.0f, "%.2f");
|
||||
if (day)
|
||||
ImGui::Text("daylight %.2f | night %.2f | %s", day->daylight(*hour), day->night(*hour),
|
||||
day->moonUp(*hour) ? "moon" : "sun");
|
||||
}
|
||||
float target = w.targetRain();
|
||||
if (ImGui::SliderFloat("Rain", &target, 0.0f, 1.0f)) w.setRain(target);
|
||||
ImGui::Text("current %.2f (R toggles)", w.rain());
|
||||
target = w.targetOvercast();
|
||||
if (ImGui::SliderFloat("Overcast", &target, 0.0f, 1.0f)) w.setOvercast(target);
|
||||
target = w.targetFog();
|
||||
if (ImGui::SliderFloat("Fog", &target, 0.0f, 1.0f)) w.setFog(target);
|
||||
bool lightning = w.lightning();
|
||||
if (ImGui::Checkbox("Lightning (while raining)", &lightning)) w.setLightning(lightning);
|
||||
ImGui::Text("current rain %.2f overcast %.2f fog %.2f", w.rain(), w.overcast(), w.fog());
|
||||
ImGui::SliderFloat("Transition (s)", &w.transitionSeconds, 0.1f, 20.0f);
|
||||
if (ImGui::TreeNode("Storm look")) {
|
||||
WeatherLook& l = w.storm;
|
||||
ImGui::SliderFloat("Sun intensity##st", &l.sunIntensity, 0.0f, 4.0f);
|
||||
ImGui::ColorEdit3("Zenith##st", glm::value_ptr(l.zenith));
|
||||
ImGui::ColorEdit3("Horizon##st", glm::value_ptr(l.horizon));
|
||||
ImGui::SliderFloat("Ambient##st", &l.ambient, 0.0f, 2.0f);
|
||||
ImGui::ColorEdit3("Fog color##st", glm::value_ptr(l.fogColor));
|
||||
ImGui::SliderFloat("Fog density##st", &l.fogDensity, 0.0f, 0.1f, "%.4f");
|
||||
ImGui::SliderFloat("Wind##st", &l.windStrength, 0.0f, 0.4f);
|
||||
ImGui::SliderFloat("Gust speed##st", &l.windGustSpeed, 0.0f, 6.0f);
|
||||
ImGui::SliderFloat("Splashes / s", &w.splashesPerSecond, 0.0f, 2000.0f);
|
||||
ImGui::SliderFloat("Night preset brightness", &w.nightBrightness, 0.0f, 1.0f);
|
||||
auto presetUI = [](const char* label, WeatherLook& l) {
|
||||
if (!ImGui::TreeNode(label)) return;
|
||||
ImGui::SliderFloat("Sun intensity", &l.sunIntensity, 0.0f, 4.0f);
|
||||
ImGui::ColorEdit3("Sun color", glm::value_ptr(l.sunColor));
|
||||
ImGui::ColorEdit3("Zenith", glm::value_ptr(l.zenith));
|
||||
ImGui::ColorEdit3("Horizon", glm::value_ptr(l.horizon));
|
||||
ImGui::SliderFloat("Ambient", &l.ambient, 0.0f, 2.0f);
|
||||
ImGui::ColorEdit3("Fog color", glm::value_ptr(l.fogColor));
|
||||
ImGui::SliderFloat("Fog density", &l.fogDensity, 0.0f, 0.2f, "%.4f");
|
||||
ImGui::SliderFloat("Fog falloff", &l.fogHeightFalloff, 0.0f, 1.5f);
|
||||
ImGui::SliderFloat("God rays", &l.godRays, 0.0f, 6.0f);
|
||||
ImGui::SliderFloat("Shadow softness", &l.shadowSoftness, 0.0f, 6.0f);
|
||||
ImGui::SliderFloat("Wind", &l.windStrength, 0.0f, 0.4f);
|
||||
ImGui::SliderFloat("Gust speed", &l.windGustSpeed, 0.0f, 6.0f);
|
||||
ImGui::TreePop();
|
||||
};
|
||||
presetUI("Storm look", w.storm);
|
||||
presetUI("Overcast look", w.overcastLook);
|
||||
presetUI("Mist look", w.mist);
|
||||
ImGui::SliderFloat("Splashes / s", &w.splashesPerSecond, 0.0f, 2000.0f);
|
||||
if (day && ImGui::TreeNode("Day keyframes")) {
|
||||
ImGui::SliderFloat("Sunrise", &day->sunrise, 3.0f, 9.0f);
|
||||
ImGui::SliderFloat("Sunset", &day->sunset, 15.0f, 21.0f);
|
||||
ImGui::SliderAngle("Moon azimuth", &day->moonAzimuth, -180.0f, 180.0f);
|
||||
ImGui::SliderAngle("Moon max elevation", &day->moonMaxElevation, 5.0f, 89.0f);
|
||||
for (DayCycle::Keyframe& k : day->keys) {
|
||||
ImGui::PushID(&k);
|
||||
if (ImGui::TreeNode(k.name)) {
|
||||
WeatherLook& l = k.look;
|
||||
ImGui::SliderFloat("Hour", &k.hour, 0.0f, 24.0f);
|
||||
ImGui::ColorEdit3("Sun / moon color", glm::value_ptr(l.sunColor));
|
||||
ImGui::SliderFloat("Sun / moon intensity", &l.sunIntensity, 0.0f, 4.0f);
|
||||
ImGui::ColorEdit3("Zenith", glm::value_ptr(l.zenith));
|
||||
ImGui::ColorEdit3("Horizon", glm::value_ptr(l.horizon));
|
||||
ImGui::ColorEdit3("Ground bounce", glm::value_ptr(l.ground));
|
||||
ImGui::SliderFloat("Ambient", &l.ambient, 0.0f, 2.0f);
|
||||
ImGui::SliderFloat("Camera fill", &l.fill, 0.0f, 1.5f);
|
||||
ImGui::ColorEdit3("Fog color", glm::value_ptr(l.fogColor));
|
||||
ImGui::SliderFloat("Fog density", &l.fogDensity, 0.0f, 0.1f, "%.4f");
|
||||
ImGui::SliderFloat("God rays", &l.godRays, 0.0f, 6.0f);
|
||||
ImGui::SliderFloat("Bloom", &l.bloomStrength, 0.0f, 0.4f);
|
||||
ImGui::SliderFloat("Saturation", &l.saturation, 0.0f, 2.0f);
|
||||
ImGui::SliderFloat("Exposure", &l.exposure, 0.1f, 4.0f);
|
||||
ImGui::ColorEdit3("Lift", glm::value_ptr(l.lift));
|
||||
ImGui::ColorEdit3("Gain", glm::value_ptr(l.gain));
|
||||
ImGui::SliderFloat("Night (stars, moon)", &l.night, 0.0f, 1.0f);
|
||||
ImGui::SliderFloat("Firefly glow", &l.fireflyGlow, 0.0f, 5.0f);
|
||||
ImGui::TreePop();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
@@ -157,6 +157,7 @@ void ParticleSystem::update(float dt, float time) {
|
||||
p.pos += wander * (0.35f * dt);
|
||||
float blink = 0.5f + 0.5f * std::sin(t * 2.3f);
|
||||
alpha *= (0.15f + 0.85f * blink * blink * blink) * ambientFade;
|
||||
color *= fireflyGlow;
|
||||
break;
|
||||
}
|
||||
case Kind::Mote: {
|
||||
|
||||
@@ -24,12 +24,12 @@ struct FrameUBO {
|
||||
glm::mat4 view, proj, viewProj, invViewProj, lightViewProj;
|
||||
glm::vec4 camPos, camParams, screen, sunDir, sunColor, skyZenith, skyHorizon, skyGround, fogColor,
|
||||
fogParams, fogParams2, wind, interactor, blob, clipPlane, billboardRight, shadowRight, cookie,
|
||||
shadowParams, shaft, waterParams, groundAO, weather;
|
||||
shadowParams, shaft, waterParams, groundAO, weather, skyParams;
|
||||
glm::ivec4 lightCount;
|
||||
glm::vec4 lightPos[kMaxLights];
|
||||
glm::vec4 lightColor[kMaxLights];
|
||||
};
|
||||
static_assert(sizeof(FrameUBO) == 5 * 64 + 23 * 16 + 16 + 2 * kMaxLights * 16, "FrameUBO must be tightly packed");
|
||||
static_assert(sizeof(FrameUBO) == 5 * 64 + 24 * 16 + 16 + 2 * kMaxLights * 16, "FrameUBO must be tightly packed");
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -174,9 +174,10 @@ struct Renderer::Impl {
|
||||
if (!b) continue;
|
||||
if (pass == 1 && !b->castShadows) continue;
|
||||
if (reflectionPass && !b->inReflections) continue;
|
||||
billboard.set("uOpaque", b->texture.opaqueBounds());
|
||||
const Texture& tex = b->tex();
|
||||
billboard.set("uOpaque", tex.opaqueBounds());
|
||||
billboard.set("uEmissive", b->emissive);
|
||||
b->texture.bind(0);
|
||||
tex.bind(0);
|
||||
b->draw();
|
||||
}
|
||||
}
|
||||
@@ -315,6 +316,7 @@ void Renderer::render(const Scene& scene, const Camera& cam, float time) {
|
||||
: glm::vec4(-1e9f, 0, 0, 0);
|
||||
f.groundAO = scene.groundOcclusion && S.groundOcclusion ? scene.groundOcclusion->rect() : glm::vec4(0.0f);
|
||||
f.weather = glm::vec4(scene.rain, scene.wetness, scene.rainSpeed, 0.0f);
|
||||
f.skyParams = glm::vec4(std::clamp(scene.sky.night, 0.0f, 1.0f), std::clamp(scene.sky.clouds, 0.0f, 1.0f), 0, 0);
|
||||
glBindTextureUnit(9, scene.groundOcclusion ? scene.groundOcclusion->texture() : I.whiteTex);
|
||||
int nl = std::min<int>(int(scene.lights.size()), kMaxLights);
|
||||
f.lightCount = glm::ivec4(nl, std::clamp(scene.volumetricLights, 0, nl), 0, 0);
|
||||
@@ -557,8 +559,8 @@ void Renderer::render(const Scene& scene, const Camera& cam, float time) {
|
||||
glDisable(GL_BLEND);
|
||||
}
|
||||
|
||||
// ---- 9. Tonemap + grade to the backbuffer --------------------------------
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
// ---- 9. Tonemap + grade to the output (backbuffer or caller FBO) --------
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, outputFbo_);
|
||||
glViewport(0, 0, w, h);
|
||||
Shader& tm = I.tonemap;
|
||||
tm.use();
|
||||
|
||||
@@ -11,13 +11,16 @@
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
bool saveScreenshot(const std::string& path, int width, int height) {
|
||||
bool saveScreenshot(const std::string& path, int width, int height, GLuint fbo) {
|
||||
std::vector<unsigned char> px(size_t(width) * height * 3);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||
glReadBuffer(GL_BACK);
|
||||
GLint prevRead = 0;
|
||||
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &prevRead);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
||||
glReadBuffer(fbo ? GL_COLOR_ATTACHMENT0 : GL_BACK);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, px.data());
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(prevRead));
|
||||
std::error_code ec;
|
||||
auto parent = std::filesystem::path(path).parent_path();
|
||||
if (!parent.empty()) std::filesystem::create_directories(parent, ec);
|
||||
@@ -28,4 +31,17 @@ bool saveScreenshot(const std::string& path, int width, int height) {
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool OffscreenTarget::create(int width, int height) {
|
||||
if (width <= 0 || height <= 0) {
|
||||
HD2D_ERR("OffscreenTarget: invalid size %dx%d", width, height);
|
||||
return false;
|
||||
}
|
||||
target_.create(width, height, {GL_RGBA8}, GL_DEPTH24_STENCIL8, 0, GL_NEAREST);
|
||||
return glCheckNamedFramebufferStatus(target_.fbo(), GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
||||
}
|
||||
|
||||
void OffscreenTarget::resize(int width, int height) {
|
||||
if (width != target_.width() || height != target_.height()) create(width, height);
|
||||
}
|
||||
|
||||
} // namespace hd2d
|
||||
|
||||
104
engine/src/SpriteAtlas.cpp
Normal file
104
engine/src/SpriteAtlas.cpp
Normal file
@@ -0,0 +1,104 @@
|
||||
#include "hd2d/SpriteAtlas.h"
|
||||
|
||||
#include "hd2d/Log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
namespace {
|
||||
constexpr int kGutter = 1; // transparent border around every cell
|
||||
}
|
||||
|
||||
bool SpriteAtlas::init(glm::ivec2 cellPixels, int cols, int rows) {
|
||||
if (cellPixels.x <= 0 || cellPixels.y <= 0 || cols <= 0 || rows <= 0) {
|
||||
HD2D_ERR("SpriteAtlas::init: invalid layout %dx%d cells of %dx%d px", cols, rows, cellPixels.x, cellPixels.y);
|
||||
return false;
|
||||
}
|
||||
cell_ = cellPixels;
|
||||
cols_ = cols;
|
||||
rows_ = rows;
|
||||
used_ = 0;
|
||||
slots_.assign(size_t(cols) * rows, Slot{});
|
||||
glm::ivec2 stride = cell_ + 2 * kGutter;
|
||||
return texture_.createEmpty(cols * stride.x, rows * stride.y);
|
||||
}
|
||||
|
||||
glm::ivec2 SpriteAtlas::origin(int slot) const {
|
||||
glm::ivec2 stride = cell_ + 2 * kGutter;
|
||||
int col = slot % cols_, rowFromTop = slot / cols_; // slot 0 = top-left, like a sheet read top-down
|
||||
return {col * stride.x + kGutter, texture_.size().y - (rowFromTop + 1) * stride.y + kGutter};
|
||||
}
|
||||
|
||||
int SpriteAtlas::add(const Image& img) {
|
||||
for (int s = 0; s < capacity(); ++s)
|
||||
if (!slots_[s].used) {
|
||||
set(s, img);
|
||||
return s;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void SpriteAtlas::set(int slot, const Image& img) {
|
||||
if (!valid(slot) || img.width < 0 || img.height < 0 || img.rgba.size() < size_t(img.width) * img.height * 4) {
|
||||
HD2D_ERR("SpriteAtlas::set: invalid slot %d (capacity %d) or image", slot, capacity());
|
||||
return;
|
||||
}
|
||||
// Compose the cell plus its gutter ring, image bottom-centered and cropped.
|
||||
const int w = std::min(img.width, cell_.x), h = std::min(img.height, cell_.y);
|
||||
const int srcX = (img.width - w) / 2; // crop the sides evenly, keep the bottom rows
|
||||
const int dstX = kGutter + (cell_.x - w) / 2, dstY = kGutter;
|
||||
Image block;
|
||||
block.width = cell_.x + 2 * kGutter;
|
||||
block.height = cell_.y + 2 * kGutter;
|
||||
block.rgba.assign(size_t(block.width) * block.height * 4, 0);
|
||||
int feetRow = -1;
|
||||
for (int y = 0; y < h; ++y) {
|
||||
const unsigned char* src = &img.rgba[(size_t(y) * img.width + srcX) * 4];
|
||||
unsigned char* dst = &block.rgba[(size_t(dstY + y) * block.width + dstX) * 4];
|
||||
std::memcpy(dst, src, size_t(w) * 4);
|
||||
if (feetRow < 0)
|
||||
for (int x = 0; x < w; ++x)
|
||||
if (src[x * 4 + 3] >= 128) {
|
||||
feetRow = y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
bleedTransparentColors(block); // padding takes edge colors: no dark fringes
|
||||
|
||||
Slot& s = slots_[slot];
|
||||
if (!s.used) ++used_;
|
||||
s.used = true;
|
||||
s.size = {w, h};
|
||||
s.feetRow = std::max(feetRow, 0);
|
||||
texture_.update(origin(slot).x - kGutter, origin(slot).y - kGutter, block);
|
||||
}
|
||||
|
||||
void SpriteAtlas::remove(int slot) {
|
||||
if (!valid(slot) || !slots_[slot].used) return;
|
||||
Image clear;
|
||||
clear.width = cell_.x + 2 * kGutter;
|
||||
clear.height = cell_.y + 2 * kGutter;
|
||||
clear.rgba.assign(size_t(clear.width) * clear.height * 4, 0);
|
||||
texture_.update(origin(slot).x - kGutter, origin(slot).y - kGutter, clear);
|
||||
slots_[slot] = Slot{};
|
||||
--used_;
|
||||
}
|
||||
|
||||
glm::vec4 SpriteAtlas::uv(int slot) const {
|
||||
if (!valid(slot)) return {0, 0, 0, 0};
|
||||
glm::vec2 ts(texture_.size()), o(origin(slot));
|
||||
return {o.x / ts.x, o.y / ts.y, (o.x + float(cell_.x)) / ts.x, (o.y + float(cell_.y)) / ts.y};
|
||||
}
|
||||
|
||||
float SpriteAtlas::anchor(int slot) const {
|
||||
if (!valid(slot)) return 0.0f;
|
||||
return float(origin(slot).y + slots_[slot].feetRow) / float(texture_.size().y);
|
||||
}
|
||||
|
||||
glm::ivec2 SpriteAtlas::imageSize(int slot) const { return valid(slot) ? slots_[slot].size : glm::ivec2(0); }
|
||||
|
||||
bool SpriteAtlas::inUse(int slot) const { return valid(slot) && slots_[slot].used; }
|
||||
|
||||
} // namespace hd2d
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
static void bleedTransparentColors(Image& img) {
|
||||
void bleedTransparentColors(Image& img) {
|
||||
const int w = img.width, h = img.height;
|
||||
std::vector<unsigned char> known(size_t(w) * h);
|
||||
for (int i = 0; i < w * h; ++i) known[i] = img.rgba[i * 4 + 3] > 0;
|
||||
@@ -71,6 +71,7 @@ Texture& Texture::operator=(Texture&& o) noexcept {
|
||||
std::swap(id_, o.id_);
|
||||
width_ = o.width_;
|
||||
height_ = o.height_;
|
||||
levels_ = o.levels_;
|
||||
opaqueBounds_ = o.opaqueBounds_;
|
||||
return *this;
|
||||
}
|
||||
@@ -79,12 +80,27 @@ Texture::~Texture() {
|
||||
if (id_) glDeleteTextures(1, &id_);
|
||||
}
|
||||
|
||||
bool Texture::load(const std::string& path, bool repeat) {
|
||||
bool Texture::load(const std::string& path, bool repeat, bool nearest) {
|
||||
Image img;
|
||||
return loadImage(path, img) && create(img, repeat);
|
||||
return loadImage(path, img) && create(img, repeat, 1.0f, nearest);
|
||||
}
|
||||
|
||||
bool Texture::create(const Image& img, bool repeat, float anisotropy) {
|
||||
// (Re)allocates immutable storage and sets sampling state.
|
||||
static GLuint allocate(GLuint old, int w, int h, int levels, bool repeat, bool nearest, float anisotropy) {
|
||||
if (old) glDeleteTextures(1, &old);
|
||||
GLuint id = 0;
|
||||
glCreateTextures(GL_TEXTURE_2D, 1, &id);
|
||||
glTextureStorage2D(id, levels, GL_SRGB8_ALPHA8, w, h);
|
||||
glTextureParameteri(id, GL_TEXTURE_MIN_FILTER, nearest ? GL_NEAREST : GL_LINEAR_MIPMAP_LINEAR);
|
||||
glTextureParameteri(id, GL_TEXTURE_MAG_FILTER, nearest ? GL_NEAREST : GL_LINEAR);
|
||||
GLint wrap = repeat ? GL_REPEAT : GL_CLAMP_TO_EDGE;
|
||||
glTextureParameteri(id, GL_TEXTURE_WRAP_S, wrap);
|
||||
glTextureParameteri(id, GL_TEXTURE_WRAP_T, wrap);
|
||||
if (anisotropy > 1.0f) glTextureParameterf(id, GL_TEXTURE_MAX_ANISOTROPY, anisotropy);
|
||||
return id;
|
||||
}
|
||||
|
||||
bool Texture::create(const Image& img, bool repeat, float anisotropy, bool nearest) {
|
||||
width_ = img.width;
|
||||
height_ = img.height;
|
||||
|
||||
@@ -99,20 +115,50 @@ bool Texture::create(const Image& img, bool repeat, float anisotropy) {
|
||||
opaqueBounds_ = {float(minX) / width_, float(minY) / height_, float(maxX + 1) / width_,
|
||||
float(maxY + 1) / height_};
|
||||
|
||||
if (id_) glDeleteTextures(1, &id_);
|
||||
glCreateTextures(GL_TEXTURE_2D, 1, &id_);
|
||||
glTextureStorage2D(id_, mipLevels(width_, height_), GL_SRGB8_ALPHA8, width_, height_);
|
||||
levels_ = nearest ? 1 : mipLevels(width_, height_);
|
||||
id_ = allocate(id_, width_, height_, levels_, repeat, nearest, anisotropy);
|
||||
glTextureSubImage2D(id_, 0, 0, 0, width_, height_, GL_RGBA, GL_UNSIGNED_BYTE, img.rgba.data());
|
||||
glGenerateTextureMipmap(id_);
|
||||
glTextureParameteri(id_, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
glTextureParameteri(id_, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
GLint wrap = repeat ? GL_REPEAT : GL_CLAMP_TO_EDGE;
|
||||
glTextureParameteri(id_, GL_TEXTURE_WRAP_S, wrap);
|
||||
glTextureParameteri(id_, GL_TEXTURE_WRAP_T, wrap);
|
||||
if (anisotropy > 1.0f) glTextureParameterf(id_, GL_TEXTURE_MAX_ANISOTROPY, anisotropy);
|
||||
if (levels_ > 1) glGenerateTextureMipmap(id_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Texture::createEmpty(int w, int h) {
|
||||
if (w <= 0 || h <= 0) {
|
||||
HD2D_ERR("Texture::createEmpty: invalid size %dx%d", w, h);
|
||||
return false;
|
||||
}
|
||||
width_ = w;
|
||||
height_ = h;
|
||||
opaqueBounds_ = {0, 0, 1, 1};
|
||||
levels_ = mipLevels(w, h);
|
||||
id_ = allocate(id_, w, h, levels_, false, false, 1.0f);
|
||||
for (int l = 0; l < levels_; ++l) glClearTexImage(id_, l, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); // transparent black
|
||||
return true;
|
||||
}
|
||||
|
||||
void Texture::update(int x, int y, const Image& img) {
|
||||
if (!id_ || x < 0 || y < 0 || x + img.width > width_ || y + img.height > height_ ||
|
||||
img.rgba.size() < size_t(img.width) * img.height * 4) {
|
||||
HD2D_ERR("Texture::update: %dx%d image at (%d, %d) does not fit %dx%d texture", img.width, img.height, x, y,
|
||||
width_, height_);
|
||||
return;
|
||||
}
|
||||
if (img.width == 0 || img.height == 0) return;
|
||||
glTextureSubImage2D(id_, 0, x, y, img.width, img.height, GL_RGBA, GL_UNSIGNED_BYTE, img.rgba.data());
|
||||
if (levels_ > 1) glGenerateTextureMipmap(id_);
|
||||
}
|
||||
|
||||
glm::vec4 Texture::cellUV(glm::ivec2 texSize, glm::ivec2 cell, int col, int rowFromTop) {
|
||||
glm::vec2 ts(texSize);
|
||||
float x0 = float(col * cell.x), y0 = float(texSize.y - (rowFromTop + 1) * cell.y); // GL bottom row
|
||||
return {x0 / ts.x, y0 / ts.y, (x0 + float(cell.x)) / ts.x, (y0 + float(cell.y)) / ts.y};
|
||||
}
|
||||
|
||||
float Texture::cellAnchor(glm::ivec2 texSize, glm::ivec2 cell, int rowFromTop, int feetRowFromCellTop) {
|
||||
int rowFromImageTop = rowFromTop * cell.y + feetRowFromCellTop;
|
||||
return float(texSize.y - (rowFromImageTop + 1)) / float(texSize.y); // bottom edge of that pixel row
|
||||
}
|
||||
|
||||
TextureArray::~TextureArray() {
|
||||
if (id_) glDeleteTextures(1, &id_);
|
||||
}
|
||||
|
||||
746
engine/src/UI.cpp
Normal file
746
engine/src/UI.cpp
Normal file
@@ -0,0 +1,746 @@
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS // before the first imgui.h include
|
||||
#include "hd2d/UI.h"
|
||||
|
||||
#include "hd2d/Log.h"
|
||||
|
||||
#include <imgui_impl_opengl3.h>
|
||||
#include <imgui_internal.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
|
||||
namespace hd2d::ui {
|
||||
namespace {
|
||||
|
||||
Fonts g_fonts;
|
||||
|
||||
// ---- colour helpers ---------------------------------------------------------
|
||||
|
||||
float alphaOf(ImU32 c) { return float((c >> IM_COL32_A_SHIFT) & 0xFF) / 255.0f; }
|
||||
|
||||
ImU32 withAlpha(ImU32 c, float a) {
|
||||
int na = int(std::clamp(alphaOf(c) * a, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
return (c & ~IM_COL32_A_MASK) | (ImU32(na) << IM_COL32_A_SHIFT);
|
||||
}
|
||||
|
||||
ImU32 rgba(int r, int g, int b, float a) { return IM_COL32(r, g, b, int(std::clamp(a, 0.0f, 1.0f) * 255.0f + 0.5f)); }
|
||||
|
||||
// Component-wise multiply (rgb and alpha).
|
||||
ImU32 modulate(ImU32 c, ImU32 t) {
|
||||
ImVec4 a = ImGui::ColorConvertU32ToFloat4(c), b = ImGui::ColorConvertU32ToFloat4(t);
|
||||
return ImGui::ColorConvertFloat4ToU32(ImVec4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w));
|
||||
}
|
||||
|
||||
ImU32 mix(ImU32 c0, ImU32 c1, float t) {
|
||||
ImVec4 a = ImGui::ColorConvertU32ToFloat4(c0), b = ImGui::ColorConvertU32ToFloat4(c1);
|
||||
return ImGui::ColorConvertFloat4ToU32(
|
||||
ImVec4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t));
|
||||
}
|
||||
|
||||
ImVec2 snap(ImVec2 p) { return {std::floor(p.x + 0.5f), std::floor(p.y + 0.5f)}; }
|
||||
|
||||
ImFont* orDefault(ImFont* f) { return f ? f : ImGui::GetFont(); }
|
||||
float displayScale() { return scaleFor(ImGui::GetIO().DisplaySize); }
|
||||
|
||||
// Fills whatever `draw` adds with a vertical gradient (vertex alpha is kept).
|
||||
template <class F>
|
||||
void verticalGradient(ImDrawList* dl, float y0, float y1, ImU32 top, ImU32 bottom, F&& draw) {
|
||||
int v0 = dl->VtxBuffer.Size;
|
||||
draw();
|
||||
ImGui::ShadeVertsLinearColorGradientKeepAlpha(dl, v0, dl->VtxBuffer.Size, ImVec2(0, y0), ImVec2(0, y1), top,
|
||||
bottom);
|
||||
}
|
||||
|
||||
// Horizontal line fading out towards both ends (or only towards the right).
|
||||
void fadeLine(ImDrawList* dl, ImVec2 a, ImVec2 b, float thickness, ImU32 col, bool bothEnds = true) {
|
||||
ImU32 clear = withAlpha(col, 0.0f);
|
||||
float y0 = a.y - thickness * 0.5f, y1 = a.y + thickness * 0.5f;
|
||||
if (bothEnds) {
|
||||
float mid = (a.x + b.x) * 0.5f;
|
||||
dl->AddRectFilledMultiColor(ImVec2(a.x, y0), ImVec2(mid, y1), clear, col, col, clear);
|
||||
dl->AddRectFilledMultiColor(ImVec2(mid, y0), ImVec2(b.x, y1), col, clear, clear, col);
|
||||
} else {
|
||||
dl->AddRectFilledMultiColor(ImVec2(a.x, y0), ImVec2(b.x, y1), col, clear, clear, col);
|
||||
}
|
||||
}
|
||||
|
||||
// Diamond gem: gold body, lighter upper-left facet, dark outline.
|
||||
void gem(ImDrawList* dl, ImVec2 c, float r, ImU32 body, ImU32 light, float alpha) {
|
||||
ImVec2 t(c.x, c.y - r), rt(c.x + r, c.y), b(c.x, c.y + r), l(c.x - r, c.y);
|
||||
dl->AddQuadFilled(t, rt, b, l, withAlpha(IM_COL32(10, 10, 16, 255), 0.75f * alpha));
|
||||
float ri = std::max(1.0f, r - 1.2f);
|
||||
ImVec2 ti(c.x, c.y - ri), rti(c.x + ri, c.y), bi(c.x, c.y + ri), li(c.x - ri, c.y);
|
||||
dl->AddQuadFilled(ti, rti, bi, li, withAlpha(body, alpha));
|
||||
dl->AddTriangleFilled(li, ti, c, withAlpha(light, alpha));
|
||||
}
|
||||
|
||||
// ---- text layout ---------------------------------------------------------------
|
||||
|
||||
struct Span {
|
||||
const char* b;
|
||||
const char* e;
|
||||
};
|
||||
|
||||
int utf8Len(unsigned char c) { return c < 0x80 ? 1 : (c >> 5) == 0x6 ? 2 : (c >> 4) == 0xE ? 3 : (c >> 3) == 0x1E ? 4 : 1; }
|
||||
|
||||
// Splits `[b, e)` into lines: hard breaks on '\n', word wrap at `wrap` px (<= 0: none).
|
||||
void layoutLines(ImFont* font, float size, const char* b, const char* e, float wrap, std::vector<Span>& out) {
|
||||
out.clear();
|
||||
const float scale = size / font->FontSize;
|
||||
const char* p = b;
|
||||
for (;;) {
|
||||
const char* nl = static_cast<const char*>(std::memchr(p, '\n', size_t(e - p)));
|
||||
const char* pe = nl ? nl : e;
|
||||
const char* s = p;
|
||||
if (s == pe) out.push_back({s, s});
|
||||
while (s < pe) {
|
||||
const char* eol = wrap > 0.0f ? font->CalcWordWrapPositionA(scale, s, pe, wrap) : pe;
|
||||
if (eol <= s) eol = std::min(pe, s + utf8Len(static_cast<unsigned char>(*s)));
|
||||
const char* le = eol;
|
||||
while (le > s && (le[-1] == ' ' || le[-1] == '\t')) --le;
|
||||
out.push_back({s, le});
|
||||
s = eol;
|
||||
while (s < pe && (*s == ' ' || *s == '\t')) ++s;
|
||||
}
|
||||
if (!nl) break;
|
||||
p = nl + 1;
|
||||
}
|
||||
}
|
||||
|
||||
float lineAdvance(float size) { return std::round(size * 1.06f); }
|
||||
|
||||
void drawSpan(ImDrawList* dl, ImFont* font, float size, ImVec2 pos, ImU32 color, const char* b, const char* e,
|
||||
bool shadow) {
|
||||
if (b >= e) return;
|
||||
pos = snap(pos);
|
||||
if (shadow) {
|
||||
float o = std::max(1.0f, std::round(size / 36.0f));
|
||||
float a = alphaOf(color);
|
||||
// Soft halo for contrast on busy backgrounds plus the crisp 1px offset shadow.
|
||||
dl->AddText(font, size, ImVec2(pos.x + o, pos.y + o * 2.0f), rgba(0, 0, 0, 0.30f * a), b, e);
|
||||
dl->AddText(font, size, ImVec2(pos.x + o, pos.y + o), rgba(4, 4, 10, 0.85f * a), b, e);
|
||||
}
|
||||
dl->AddText(font, size, pos, color, b, e);
|
||||
}
|
||||
|
||||
// ---- option list (menu + dialogue choices) ----------------------------------------
|
||||
|
||||
struct ListMetrics {
|
||||
float s, size, titleSize, rowH, padX, padY, cursorW, titleH;
|
||||
ImVec2 panel;
|
||||
};
|
||||
|
||||
ListMetrics listMetrics(const std::vector<std::string>& items, std::string_view title, float scale) {
|
||||
ListMetrics m;
|
||||
m.s = scale;
|
||||
m.size = std::round(kBodySize * 0.9f * scale);
|
||||
m.titleSize = std::round(kNameSize * scale);
|
||||
m.rowH = std::round(m.size * 1.3f);
|
||||
m.padX = std::round(26.0f * scale);
|
||||
m.padY = std::round(18.0f * scale);
|
||||
m.cursorW = std::round(30.0f * scale);
|
||||
m.titleH = title.empty() ? 0.0f : std::round(m.titleSize + 22.0f * scale);
|
||||
ImFont* body = orDefault(g_fonts.body);
|
||||
float w = 0.0f;
|
||||
for (const std::string& it : items) w = std::max(w, measure(body, m.size, it).x);
|
||||
if (!title.empty()) w = std::max(w, measure(orDefault(g_fonts.name), m.titleSize, title).x - m.cursorW);
|
||||
m.panel.x = std::round(std::max(w + m.cursorW + m.padX * 2.0f + 8.0f * scale, 200.0f * scale));
|
||||
m.panel.y = std::round(m.padY * 2.0f + m.titleH + m.rowH * float(items.size()));
|
||||
return m;
|
||||
}
|
||||
|
||||
void drawList(ImDrawList* dl, ImVec2 min, const ListMetrics& m, const std::vector<std::string>& items, int selected,
|
||||
std::string_view title) {
|
||||
const float s = m.s;
|
||||
ImVec2 max = min + m.panel;
|
||||
PanelStyle ps;
|
||||
ps.scale = s;
|
||||
panel(dl, min, max, ps);
|
||||
|
||||
float y = min.y + m.padY;
|
||||
if (!title.empty()) {
|
||||
textCentered(dl, g_fonts.name, m.titleSize, ImVec2((min.x + max.x) * 0.5f, y + m.titleSize * 0.5f), kGold,
|
||||
title);
|
||||
float ly = std::round(y + m.titleSize + 10.0f * s) + 0.5f;
|
||||
fadeLine(dl, ImVec2(min.x + 14.0f * s, ly), ImVec2(max.x - 14.0f * s, ly), std::max(1.0f, s), kGoldDim);
|
||||
y += m.titleH;
|
||||
}
|
||||
|
||||
const float t = float(ImGui::GetTime());
|
||||
ImFont* body = orDefault(g_fonts.body);
|
||||
for (int i = 0; i < int(items.size()); ++i) {
|
||||
ImVec2 r0(min.x + 8.0f * s, y), r1(max.x - 8.0f * s, y + m.rowH);
|
||||
bool sel = i == selected;
|
||||
float textY = y + (m.rowH - m.size) * 0.5f;
|
||||
if (sel) {
|
||||
ImU32 hi = IM_COL32(222, 189, 120, 70), clear = IM_COL32(222, 189, 120, 0);
|
||||
dl->AddRectFilledMultiColor(r0, r1, hi, clear, clear, hi);
|
||||
fadeLine(dl, ImVec2(r0.x, r0.y + 0.5f), ImVec2(r1.x, r0.y + 0.5f), 1.0f, IM_COL32(222, 189, 120, 110),
|
||||
false);
|
||||
fadeLine(dl, ImVec2(r0.x, r1.y - 0.5f), ImVec2(r1.x, r1.y - 0.5f), 1.0f, IM_COL32(222, 189, 120, 110),
|
||||
false);
|
||||
// Right-pointing gold cursor that bobs gently.
|
||||
float bob = std::sin(t * 6.0f) * 2.5f * s;
|
||||
float cx = min.x + m.padX + 4.0f * s + bob, cy = y + m.rowH * 0.5f, h = 8.0f * s;
|
||||
ImVec2 a(cx - h * 0.6f, cy - h), b(cx + h * 0.9f, cy), c(cx - h * 0.6f, cy + h);
|
||||
dl->AddTriangleFilled(a + ImVec2(1, 1.5f) * s, b + ImVec2(1, 1.5f) * s, c + ImVec2(1, 1.5f) * s,
|
||||
IM_COL32(0, 0, 0, 150));
|
||||
dl->AddTriangleFilled(a, b, c, kGold);
|
||||
dl->AddTriangleFilled(a, b, ImVec2(cx - h * 0.6f, cy), IM_COL32(255, 238, 190, 255));
|
||||
}
|
||||
drawSpan(dl, body, m.size, ImVec2(min.x + m.padX + m.cursorW, textY), sel ? IM_COL32(255, 248, 230, 255) : kTextMuted,
|
||||
items[size_t(i)].data(), items[size_t(i)].data() + items[size_t(i)].size(), true);
|
||||
y += m.rowH;
|
||||
}
|
||||
}
|
||||
|
||||
bool isSentenceEnd(const char* p, size_t len) {
|
||||
if (len == 1) return *p == '.' || *p == '!' || *p == '?';
|
||||
return len == 3 && !std::memcmp(p, "\xE2\x80\xA6", 3); // ellipsis
|
||||
}
|
||||
bool isClauseEnd(const char* p, size_t len) {
|
||||
if (len == 1) return *p == ',' || *p == ';' || *p == ':';
|
||||
return len == 3 && (!std::memcmp(p, "\xE2\x80\x94", 3) || !std::memcmp(p, "\xE2\x80\x93", 3)); // em/en dash
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- fonts / theme -------------------------------------------------------------------
|
||||
|
||||
bool loadFonts(const std::string& fontDir, float pixelScale) {
|
||||
static const ImWchar kRanges[] = {
|
||||
0x0020, 0x00FF, // Basic Latin + Latin-1
|
||||
0x0100, 0x017F, // Latin Extended-A
|
||||
0x2010, 0x2027, // dashes, quotes, bullets, ellipsis
|
||||
0x2030, 0x203A, // per mille, guillemets
|
||||
0x20AC, 0x20AC, // euro
|
||||
0x2122, 0x2122, // trade mark
|
||||
0,
|
||||
};
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImFontAtlas* atlas = io.Fonts;
|
||||
if (io.BackendRendererUserData) ImGui_ImplOpenGL3_DestroyFontsTexture();
|
||||
atlas->Clear();
|
||||
atlas->AddFontDefault(); // stays io.FontDefault (first font) for debug panels
|
||||
io.FontDefault = nullptr;
|
||||
g_fonts = {};
|
||||
|
||||
pixelScale = std::max(pixelScale, 0.25f);
|
||||
bool ok = true;
|
||||
auto add = [&](const char* file, float size, float multiply) -> ImFont* {
|
||||
std::string path = fontDir + "/" + file;
|
||||
if (!std::filesystem::exists(path)) {
|
||||
HD2D_ERR("ui: missing font %s", path.c_str());
|
||||
ok = false;
|
||||
return nullptr;
|
||||
}
|
||||
ImFontConfig cfg;
|
||||
cfg.OversampleH = 2;
|
||||
cfg.OversampleV = 1;
|
||||
cfg.RasterizerMultiply = multiply;
|
||||
std::snprintf(cfg.Name, sizeof cfg.Name, "%s %.0fpx", file, size * pixelScale);
|
||||
return atlas->AddFontFromFileTTF(path.c_str(), std::round(size * pixelScale), &cfg, kRanges);
|
||||
};
|
||||
g_fonts.body = add("EBGaramond.ttf", kBodySize, 1.15f);
|
||||
g_fonts.small = add("EBGaramond.ttf", kSmallSize, 1.2f);
|
||||
g_fonts.name = add("Cinzel.ttf", kNameSize, 1.1f);
|
||||
g_fonts.title = add("CinzelDecorative-Bold.ttf", kTitleSize, 1.0f);
|
||||
// The backend re-uploads the atlas on the next NewFrame().
|
||||
return ok;
|
||||
}
|
||||
|
||||
const Fonts& fonts() { return g_fonts; }
|
||||
|
||||
float scaleFor(ImVec2 viewport) { return viewport.y > 0.0f ? viewport.y / 1080.0f : 1.0f; }
|
||||
|
||||
void applyTheme() {
|
||||
ImGuiStyle& st = ImGui::GetStyle();
|
||||
st.WindowRounding = 6.0f;
|
||||
st.ChildRounding = 4.0f;
|
||||
st.FrameRounding = 4.0f;
|
||||
st.PopupRounding = 4.0f;
|
||||
st.GrabRounding = 3.0f;
|
||||
st.TabRounding = 4.0f;
|
||||
st.ScrollbarRounding = 6.0f;
|
||||
st.WindowBorderSize = 1.0f;
|
||||
st.FrameBorderSize = 1.0f;
|
||||
st.PopupBorderSize = 1.0f;
|
||||
st.WindowPadding = ImVec2(12, 10);
|
||||
st.FramePadding = ImVec2(8, 4);
|
||||
st.ItemSpacing = ImVec2(8, 6);
|
||||
st.WindowTitleAlign = ImVec2(0.5f, 0.5f);
|
||||
|
||||
auto c = [](int r, int g, int b, float a = 1.0f) { return ImVec4(r / 255.0f, g / 255.0f, b / 255.0f, a); };
|
||||
ImVec4* k = st.Colors;
|
||||
const ImVec4 gold = c(222, 189, 120), goldDim = c(150, 122, 74);
|
||||
k[ImGuiCol_Text] = c(246, 240, 226);
|
||||
k[ImGuiCol_TextDisabled] = c(140, 134, 120);
|
||||
k[ImGuiCol_WindowBg] = c(14, 17, 30, 0.92f);
|
||||
k[ImGuiCol_ChildBg] = c(0, 0, 0, 0.0f);
|
||||
k[ImGuiCol_PopupBg] = c(12, 14, 26, 0.96f);
|
||||
k[ImGuiCol_Border] = c(150, 122, 74, 0.70f);
|
||||
k[ImGuiCol_BorderShadow] = c(0, 0, 0, 0.0f);
|
||||
k[ImGuiCol_FrameBg] = c(28, 32, 52, 0.85f);
|
||||
k[ImGuiCol_FrameBgHovered] = c(44, 48, 72, 0.90f);
|
||||
k[ImGuiCol_FrameBgActive] = c(58, 56, 70, 1.0f);
|
||||
k[ImGuiCol_TitleBg] = c(20, 22, 36, 0.95f);
|
||||
k[ImGuiCol_TitleBgActive] = c(46, 38, 26, 0.98f);
|
||||
k[ImGuiCol_TitleBgCollapsed] = c(14, 17, 30, 0.75f);
|
||||
k[ImGuiCol_MenuBarBg] = c(20, 22, 36, 1.0f);
|
||||
k[ImGuiCol_ScrollbarBg] = c(8, 10, 18, 0.6f);
|
||||
k[ImGuiCol_ScrollbarGrab] = c(110, 92, 60, 0.8f);
|
||||
k[ImGuiCol_ScrollbarGrabHovered] = goldDim;
|
||||
k[ImGuiCol_ScrollbarGrabActive] = gold;
|
||||
k[ImGuiCol_CheckMark] = gold;
|
||||
k[ImGuiCol_SliderGrab] = goldDim;
|
||||
k[ImGuiCol_SliderGrabActive] = gold;
|
||||
k[ImGuiCol_Button] = c(40, 42, 62, 0.90f);
|
||||
k[ImGuiCol_ButtonHovered] = c(96, 78, 46, 0.95f);
|
||||
k[ImGuiCol_ButtonActive] = c(150, 122, 74, 1.0f);
|
||||
k[ImGuiCol_Header] = c(96, 78, 46, 0.55f);
|
||||
k[ImGuiCol_HeaderHovered] = c(120, 98, 58, 0.75f);
|
||||
k[ImGuiCol_HeaderActive] = c(150, 122, 74, 0.90f);
|
||||
k[ImGuiCol_Separator] = c(150, 122, 74, 0.55f);
|
||||
k[ImGuiCol_SeparatorHovered] = goldDim;
|
||||
k[ImGuiCol_SeparatorActive] = gold;
|
||||
k[ImGuiCol_ResizeGrip] = c(150, 122, 74, 0.25f);
|
||||
k[ImGuiCol_ResizeGripHovered] = c(150, 122, 74, 0.65f);
|
||||
k[ImGuiCol_ResizeGripActive] = gold;
|
||||
k[ImGuiCol_Tab] = c(30, 32, 50, 0.90f);
|
||||
k[ImGuiCol_TabHovered] = c(120, 98, 58, 0.85f);
|
||||
k[ImGuiCol_TabSelected] = c(86, 70, 42, 1.0f);
|
||||
k[ImGuiCol_TabSelectedOverline] = gold;
|
||||
k[ImGuiCol_TabDimmed] = c(20, 22, 36, 0.90f);
|
||||
k[ImGuiCol_TabDimmedSelected] = c(52, 44, 32, 1.0f);
|
||||
k[ImGuiCol_PlotLines] = goldDim;
|
||||
k[ImGuiCol_PlotLinesHovered] = gold;
|
||||
k[ImGuiCol_PlotHistogram] = goldDim;
|
||||
k[ImGuiCol_PlotHistogramHovered] = gold;
|
||||
k[ImGuiCol_TableHeaderBg] = c(30, 32, 50, 1.0f);
|
||||
k[ImGuiCol_TableBorderStrong] = c(150, 122, 74, 0.6f);
|
||||
k[ImGuiCol_TableBorderLight] = c(150, 122, 74, 0.3f);
|
||||
k[ImGuiCol_TableRowBg] = c(0, 0, 0, 0.0f);
|
||||
k[ImGuiCol_TableRowBgAlt] = c(255, 255, 255, 0.03f);
|
||||
k[ImGuiCol_TextSelectedBg] = c(150, 122, 74, 0.45f);
|
||||
k[ImGuiCol_DragDropTarget] = gold;
|
||||
k[ImGuiCol_NavCursor] = gold;
|
||||
k[ImGuiCol_NavWindowingHighlight] = c(255, 255, 255, 0.7f);
|
||||
k[ImGuiCol_NavWindowingDimBg] = c(0, 0, 0, 0.4f);
|
||||
k[ImGuiCol_ModalWindowDimBg] = c(0, 0, 0, 0.55f);
|
||||
}
|
||||
|
||||
// ---- primitives ------------------------------------------------------------------------
|
||||
|
||||
void panel(ImDrawList* dl, ImVec2 min, ImVec2 max, const PanelStyle& style) {
|
||||
const float s = style.scale > 0.0f ? style.scale : displayScale();
|
||||
const float fade = alphaOf(style.tint);
|
||||
if (fade <= 0.0f) return;
|
||||
min = snap(min);
|
||||
max = snap(max);
|
||||
const float r = style.rounding * s;
|
||||
|
||||
// Soft drop shadow: stacked, expanding translucent rounded rects.
|
||||
const ImVec2 off(0.0f, std::round(5.0f * s));
|
||||
const float spread = 14.0f * s;
|
||||
for (int i = 8; i >= 1; --i) {
|
||||
float e = spread * float(i) / 8.0f;
|
||||
dl->AddRectFilled(min + off - ImVec2(e, e), max + off + ImVec2(e, e), rgba(0, 0, 0, 0.045f * fade), r + e);
|
||||
}
|
||||
|
||||
// Navy -> near-black vertical gradient body.
|
||||
verticalGradient(dl, min.y, max.y, IM_COL32(32, 39, 66, 255), IM_COL32(8, 10, 21, 255),
|
||||
[&] { dl->AddRectFilled(min, max, rgba(255, 255, 255, style.alpha * fade), r); });
|
||||
// Faint top sheen.
|
||||
fadeLine(dl, ImVec2(min.x + r, min.y + 2.0f * s), ImVec2(max.x - r, min.y + 2.0f * s), std::max(1.0f, s),
|
||||
rgba(255, 255, 255, 0.10f * fade));
|
||||
|
||||
// Thin warm-gold double border.
|
||||
const ImU32 gold = withAlpha(modulate(kGold, style.tint | IM_COL32_A_MASK), fade);
|
||||
const ImU32 goldDim = withAlpha(modulate(kGoldDim, style.tint | IM_COL32_A_MASK), 0.85f * fade);
|
||||
const float outer = std::max(1.0f, std::round(1.5f * s));
|
||||
const float inset = std::round(5.0f * s);
|
||||
const ImVec2 h(0.5f, 0.5f);
|
||||
dl->AddRect(min + h, max - h, gold, r, 0, outer);
|
||||
dl->AddRect(min + ImVec2(inset, inset) + h, max - ImVec2(inset, inset) - h, goldDim, std::max(0.0f, r - inset), 0,
|
||||
std::max(1.0f, s));
|
||||
|
||||
if (!style.ornate) return;
|
||||
// Corner gems sitting across both border lines, bright L-brackets along the
|
||||
// inner rule, plus a small gem centred on the top and bottom rules.
|
||||
const float g = 6.5f * s, arm = std::round(24.0f * s), c = std::round(inset * 0.5f) + 0.5f;
|
||||
const ImU32 light = withAlpha(IM_COL32(255, 240, 200, 255), fade);
|
||||
const ImVec2 corners[4] = {{min.x + c, min.y + c}, {max.x - c, min.y + c}, {max.x - c, max.y - c}, {min.x + c, max.y - c}};
|
||||
const ImVec2 dirs[4] = {{1, 1}, {-1, 1}, {-1, -1}, {1, -1}};
|
||||
const float lt = std::max(1.0f, std::round(2.0f * s));
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
ImVec2 p = corners[i], d = dirs[i];
|
||||
ImVec2 q(p.x + d.x * (inset - c + 1.0f), p.y + d.y * (inset - c + 1.0f)); // on the inner rule
|
||||
dl->AddLine(ImVec2(q.x + d.x * g * 0.6f, q.y), ImVec2(q.x + d.x * arm, q.y), gold, lt);
|
||||
dl->AddLine(ImVec2(q.x, q.y + d.y * g * 0.6f), ImVec2(q.x, q.y + d.y * arm), gold, lt);
|
||||
dl->AddCircleFilled(ImVec2(q.x + d.x * (arm + 3.0f * s), q.y), 1.6f * s, gold);
|
||||
dl->AddCircleFilled(ImVec2(q.x, q.y + d.y * (arm + 3.0f * s)), 1.6f * s, gold);
|
||||
gem(dl, p, g, gold, light, 1.0f);
|
||||
}
|
||||
if (max.x - min.x > 240.0f * s) {
|
||||
float cx = std::round((min.x + max.x) * 0.5f);
|
||||
gem(dl, ImVec2(cx, min.y + 0.5f), 3.5f * s, gold, light, 1.0f);
|
||||
gem(dl, ImVec2(cx, max.y - 0.5f), 3.5f * s, gold, light, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void text(ImDrawList* dl, ImFont* font, float size, ImVec2 pos, ImU32 color, std::string_view s, bool shadow) {
|
||||
drawSpan(dl, orDefault(font), size, pos, color, s.data(), s.data() + s.size(), shadow);
|
||||
}
|
||||
|
||||
void textWrapped(ImDrawList* dl, ImFont* font, float size, ImVec2 pos, ImU32 color, std::string_view s,
|
||||
float wrapWidth, bool shadow) {
|
||||
font = orDefault(font);
|
||||
static thread_local std::vector<Span> lines;
|
||||
layoutLines(font, size, s.data(), s.data() + s.size(), wrapWidth, lines);
|
||||
for (const Span& l : lines) {
|
||||
drawSpan(dl, font, size, pos, color, l.b, l.e, shadow);
|
||||
pos.y += lineAdvance(size);
|
||||
}
|
||||
}
|
||||
|
||||
ImVec2 measure(ImFont* font, float size, std::string_view s, float wrapWidth) {
|
||||
font = orDefault(font);
|
||||
if (wrapWidth <= 0.0f) return font->CalcTextSizeA(size, FLT_MAX, 0.0f, s.data(), s.data() + s.size());
|
||||
static thread_local std::vector<Span> lines;
|
||||
layoutLines(font, size, s.data(), s.data() + s.size(), wrapWidth, lines);
|
||||
float w = 0.0f;
|
||||
for (const Span& l : lines) w = std::max(w, font->CalcTextSizeA(size, FLT_MAX, 0.0f, l.b, l.e).x);
|
||||
return ImVec2(w, lineAdvance(size) * float(lines.size() - 1) + size);
|
||||
}
|
||||
|
||||
void textCentered(ImDrawList* dl, ImFont* font, float size, ImVec2 center, ImU32 color, std::string_view s,
|
||||
bool shadow) {
|
||||
ImVec2 sz = measure(font, size, s);
|
||||
text(dl, font, size, ImVec2(center.x - sz.x * 0.5f, center.y - sz.y * 0.5f), color, s, shadow);
|
||||
}
|
||||
|
||||
void titleText(ImDrawList* dl, ImVec2 center, std::string_view s, float size) {
|
||||
ImFont* font = orDefault(g_fonts.title);
|
||||
const char* b = s.data();
|
||||
const char* e = b + s.size();
|
||||
ImVec2 sz = font->CalcTextSizeA(size, FLT_MAX, 0.0f, b, e);
|
||||
ImVec2 pos = snap(ImVec2(center.x - sz.x * 0.5f, center.y - sz.y * 0.5f));
|
||||
const float u = size / 96.0f;
|
||||
|
||||
// Soft dark halo (legible on bright scenery), then a warm glow around the glyphs.
|
||||
for (int ring = 0; ring < 3; ++ring) {
|
||||
float rad = (ring == 0 ? 10.0f : ring == 1 ? 6.0f : 3.5f) * u;
|
||||
ImU32 col = ring == 0 ? IM_COL32(0, 0, 0, 22) : ring == 1 ? IM_COL32(10, 6, 0, 34) : IM_COL32(255, 200, 110, 46);
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
float a = float(i) * 3.14159265f / 6.0f;
|
||||
dl->AddText(font, size, pos + ImVec2(std::cos(a), std::sin(a)) * rad, col, b, e);
|
||||
}
|
||||
}
|
||||
// Dark outline + drop shadow keep the letterforms crisp against the glow.
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
float a = float(i) * 3.14159265f / 4.0f;
|
||||
dl->AddText(font, size, pos + ImVec2(std::cos(a), std::sin(a)) * (2.0f * u), IM_COL32(20, 12, 4, 200), b, e);
|
||||
}
|
||||
dl->AddText(font, size, pos + ImVec2(0.0f, 4.0f * u), IM_COL32(0, 0, 0, 150), b, e);
|
||||
// Gold face: pale highlight at the top to deep gold at the bottom.
|
||||
verticalGradient(dl, pos.y + sz.y * 0.2f, pos.y + sz.y * 0.85f, IM_COL32(255, 246, 214, 255),
|
||||
IM_COL32(200, 142, 52, 255), [&] { dl->AddText(font, size, pos, IM_COL32_WHITE, b, e); });
|
||||
|
||||
// Flourish rule underneath: fading gold lines with a centre gem, on a dark underlay.
|
||||
float y = std::round(pos.y + sz.y * 0.95f) + 0.5f;
|
||||
float half = sz.x * 0.5f + 40.0f * u;
|
||||
float cx = center.x;
|
||||
const float lt = std::max(1.0f, std::round(2.0f * u));
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
ImU32 col = pass ? kGold : IM_COL32(0, 0, 0, 110);
|
||||
ImU32 clear = withAlpha(col, 0.0f);
|
||||
float t = pass ? lt : lt + 2.0f * std::max(1.0f, u);
|
||||
dl->AddRectFilledMultiColor(ImVec2(cx - half, y - t * 0.5f), ImVec2(cx - 12.0f * u, y + t * 0.5f), clear, col,
|
||||
col, clear);
|
||||
fadeLine(dl, ImVec2(cx + 12.0f * u, y), ImVec2(cx + half, y), t, col, false);
|
||||
}
|
||||
gem(dl, ImVec2(cx, y), 8.0f * u, kGold, IM_COL32(255, 240, 200, 255), 1.0f);
|
||||
gem(dl, ImVec2(cx - 24.0f * u, y), 4.0f * u, kGold, IM_COL32(255, 240, 200, 255), 1.0f);
|
||||
gem(dl, ImVec2(cx + 24.0f * u, y), 4.0f * u, kGold, IM_COL32(255, 240, 200, 255), 1.0f);
|
||||
}
|
||||
|
||||
void nameplate(ImDrawList* dl, ImVec2 anchor, std::string_view s, ImU32 accent, float scale) {
|
||||
ImFont* font = orDefault(g_fonts.name);
|
||||
const float size = std::round(kNameSize * 0.82f * scale);
|
||||
ImVec2 ts = measure(font, size, s);
|
||||
const float padX = std::round(16.0f * scale), padY = std::round(6.0f * scale), tip = std::round(8.0f * scale);
|
||||
ImVec2 sz(std::round(ts.x + padX * 2.0f), std::round(size + padY * 2.0f));
|
||||
ImVec2 min = snap(ImVec2(anchor.x - sz.x * 0.5f, anchor.y - tip - sz.y));
|
||||
ImVec2 max = min + sz;
|
||||
const float r = sz.y * 0.5f;
|
||||
|
||||
dl->AddRectFilled(min + ImVec2(0, 3.0f * scale) - ImVec2(2, 2) * scale, max + ImVec2(0, 3.0f * scale) + ImVec2(2, 2) * scale,
|
||||
IM_COL32(0, 0, 0, 70), r + 2.0f * scale);
|
||||
// Pointer below the pill.
|
||||
ImVec2 t0(anchor.x - tip, max.y - 1.0f), t1(anchor.x + tip, max.y - 1.0f), t2(anchor.x, anchor.y);
|
||||
dl->AddTriangleFilled(t0 - ImVec2(1.5f, 0) * scale, t1 + ImVec2(1.5f, 0) * scale, t2 + ImVec2(0, 2.0f) * scale, accent);
|
||||
dl->AddTriangleFilled(t0 + ImVec2(1.5f, 0) * scale, t1 - ImVec2(1.5f, 0) * scale, t2 - ImVec2(0, 1.0f) * scale,
|
||||
IM_COL32(10, 12, 24, 235));
|
||||
verticalGradient(dl, min.y, max.y, IM_COL32(34, 40, 66, 255), IM_COL32(10, 12, 24, 255),
|
||||
[&] { dl->AddRectFilled(min, max, IM_COL32(255, 255, 255, 230), r); });
|
||||
dl->AddRect(min + ImVec2(0.5f, 0.5f), max - ImVec2(0.5f, 0.5f), accent, r, 0, std::max(1.0f, std::round(1.5f * scale)));
|
||||
text(dl, font, size, ImVec2(min.x + padX, min.y + padY - 1.0f * scale), kTextColor, s);
|
||||
}
|
||||
|
||||
ImVec2 menuSize(const std::vector<std::string>& items, std::string_view title, float scale) {
|
||||
return listMetrics(items, title, scale).panel;
|
||||
}
|
||||
|
||||
void menuAt(ImDrawList* dl, ImVec2 pos, ImVec2 pivot, const std::vector<std::string>& items, int selected,
|
||||
std::string_view title, float scale) {
|
||||
ListMetrics m = listMetrics(items, title, scale);
|
||||
drawList(dl, ImVec2(pos.x - m.panel.x * pivot.x, pos.y - m.panel.y * pivot.y), m, items, selected, title);
|
||||
}
|
||||
|
||||
void menu(ImDrawList* dl, ImVec2 center, const std::vector<std::string>& items, int selected, std::string_view title,
|
||||
float scale) {
|
||||
menuAt(dl, center, ImVec2(0.5f, 0.5f), items, selected, title, scale);
|
||||
}
|
||||
|
||||
void progressBar(ImDrawList* dl, ImVec2 min, ImVec2 max, float t, ImU32 color) {
|
||||
min = snap(min);
|
||||
max = snap(max);
|
||||
const float h = max.y - min.y, r = h * 0.5f;
|
||||
dl->AddRectFilled(min, max, IM_COL32(6, 8, 14, 225), r);
|
||||
const float in = std::max(1.0f, std::round(h * 0.18f));
|
||||
ImVec2 f0(min.x + in, min.y + in), f1(max.x - in, max.y - in);
|
||||
float w = (f1.x - f0.x) * std::clamp(t, 0.0f, 1.0f);
|
||||
if (w > 0.5f) {
|
||||
f1.x = f0.x + w;
|
||||
float fr = std::min(r - in, w * 0.5f);
|
||||
verticalGradient(dl, f0.y, f1.y, mix(color, IM_COL32_WHITE, 0.45f), mix(color, IM_COL32_BLACK, 0.25f),
|
||||
[&] { dl->AddRectFilled(f0, f1, IM_COL32(255, 255, 255, int(alphaOf(color) * 255.0f)), fr); });
|
||||
float hy = f0.y + std::max(1.0f, (f1.y - f0.y) * 0.25f);
|
||||
if (w > 2.0f * fr)
|
||||
dl->AddLine(ImVec2(f0.x + fr, hy), ImVec2(f1.x - fr, hy), IM_COL32(255, 255, 255, 70), 1.0f);
|
||||
}
|
||||
dl->AddRect(min + ImVec2(0.5f, 0.5f), max - ImVec2(0.5f, 0.5f), withAlpha(kGoldDim, 0.9f), r, 0, 1.0f);
|
||||
}
|
||||
|
||||
void advanceIndicator(ImDrawList* dl, ImVec2 c, float scale, float time) {
|
||||
const float bob = std::sin(time * 4.5f) * 2.5f * scale;
|
||||
const float a = 0.6f + 0.4f * std::cos(time * 4.5f);
|
||||
const float w = 8.0f * scale, h = 10.0f * scale;
|
||||
c.y += bob;
|
||||
ImVec2 l(c.x - w, c.y - h * 0.4f), r(c.x + w, c.y - h * 0.4f), b(c.x, c.y + h * 0.6f), top(c.x, c.y - h * 0.9f);
|
||||
ImVec2 sh(1.0f * scale, 2.0f * scale);
|
||||
dl->AddQuadFilled(top + sh, r + sh, b + sh, l + sh, rgba(0, 0, 0, 0.55f * a));
|
||||
dl->AddQuadFilled(top, r, b, l, withAlpha(kGold, a));
|
||||
dl->AddTriangleFilled(l, top, ImVec2(c.x, c.y - h * 0.4f), withAlpha(IM_COL32(255, 244, 210, 255), a));
|
||||
dl->AddTriangleFilled(ImVec2(c.x, c.y - h * 0.4f), r, b, withAlpha(IM_COL32(170, 128, 60, 255), a));
|
||||
}
|
||||
|
||||
// ---- DialogueBox ---------------------------------------------------------------------------
|
||||
|
||||
void DialogueBox::start(std::vector<DialogueLine> lines, std::vector<std::string> choices) {
|
||||
lines_ = std::move(lines);
|
||||
if (lines_.empty()) lines_.emplace_back();
|
||||
choices_ = std::move(choices);
|
||||
line_ = 0;
|
||||
revealed_ = 0;
|
||||
budget_ = 0.0f;
|
||||
selected_ = 0;
|
||||
chosen_ = -1;
|
||||
open_ = true;
|
||||
}
|
||||
|
||||
bool DialogueBox::lineRevealed() const { return !open_ || revealed_ >= lines_[line_].text.size(); }
|
||||
|
||||
bool DialogueBox::choicesVisible() const {
|
||||
return open_ && !choices_.empty() && line_ + 1 == lines_.size() && lineRevealed();
|
||||
}
|
||||
|
||||
void DialogueBox::stepChar() {
|
||||
const std::string& t = lines_[line_].text;
|
||||
const char* p = t.data() + revealed_;
|
||||
size_t len = std::min(size_t(utf8Len(static_cast<unsigned char>(*p))), t.size() - revealed_);
|
||||
// Swallow malformed continuation bytes so we always land on a boundary.
|
||||
while (revealed_ + len < t.size() && (static_cast<unsigned char>(t[revealed_ + len]) & 0xC0) == 0x80) ++len;
|
||||
revealed_ += len;
|
||||
bool atBreak = revealed_ >= t.size() || t[revealed_] == ' ' || t[revealed_] == '\n' || t[revealed_] == '"' ||
|
||||
t[revealed_] == '\'' || t[revealed_] == ')';
|
||||
if (atBreak && revealed_ < t.size()) {
|
||||
if (isSentenceEnd(p, len)) budget_ -= sentencePause;
|
||||
else if (isClauseEnd(p, len)) budget_ -= clausePause;
|
||||
}
|
||||
}
|
||||
|
||||
void DialogueBox::update(float dt) {
|
||||
if (!open_) return;
|
||||
time_ += dt;
|
||||
if (lineRevealed()) {
|
||||
budget_ = 0.0f;
|
||||
return;
|
||||
}
|
||||
budget_ += dt;
|
||||
const float cost = 1.0f / std::max(1.0f, charsPerSecond);
|
||||
while (!lineRevealed() && budget_ >= cost) {
|
||||
budget_ -= cost;
|
||||
stepChar();
|
||||
}
|
||||
}
|
||||
|
||||
bool DialogueBox::advance() {
|
||||
if (!open_) return false;
|
||||
if (!lineRevealed()) {
|
||||
revealed_ = lines_[line_].text.size();
|
||||
return false;
|
||||
}
|
||||
if (line_ + 1 < lines_.size()) {
|
||||
++line_;
|
||||
revealed_ = 0;
|
||||
budget_ = 0.0f;
|
||||
return false;
|
||||
}
|
||||
chosen_ = choices_.empty() ? -1 : selected_;
|
||||
open_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DialogueBox::moveChoice(int delta) {
|
||||
if (!choicesVisible()) return;
|
||||
int n = int(choices_.size());
|
||||
selected_ = ((selected_ + delta) % n + n) % n;
|
||||
}
|
||||
|
||||
void DialogueBox::close() {
|
||||
open_ = false;
|
||||
chosen_ = -1;
|
||||
}
|
||||
|
||||
void DialogueBox::draw(ImVec2 viewport) {
|
||||
if (!open_) return;
|
||||
ImDrawList* dl = ImGui::GetForegroundDrawList();
|
||||
const DialogueLine& line = lines_[line_];
|
||||
const float s = scaleFor(viewport);
|
||||
ImFont* body = orDefault(g_fonts.body);
|
||||
const float size = std::round(kBodySize * s);
|
||||
const float lh = lineAdvance(size);
|
||||
const float padX = std::round(48.0f * s), padY = std::round(30.0f * s);
|
||||
|
||||
const float boxW = std::round(std::min(viewport.x * 0.70f, viewport.y * 1.6f));
|
||||
float textX = padX, textW = boxW - padX * 2.0f;
|
||||
const float minTextH = lh * 2.0f + size; // room for three lines
|
||||
float portrait = 0.0f;
|
||||
if (line.portrait) {
|
||||
portrait = std::round(minTextH + padY * 2.0f - 28.0f * s);
|
||||
textX = std::round(20.0f * s + portrait + 30.0f * s);
|
||||
textW = boxW - textX - padX;
|
||||
}
|
||||
|
||||
static thread_local std::vector<Span> spans;
|
||||
const char* tb = line.text.data();
|
||||
const char* te = tb + line.text.size();
|
||||
layoutLines(body, size, tb, te, textW, spans);
|
||||
const float textH = std::max(minTextH, lh * float(spans.size() - 1) + size);
|
||||
const float boxH = std::round(textH + padY * 2.0f);
|
||||
const ImVec2 min = snap(ImVec2((viewport.x - boxW) * 0.5f, viewport.y - std::round(40.0f * s) - boxH));
|
||||
const ImVec2 max = min + ImVec2(boxW, boxH);
|
||||
|
||||
PanelStyle ps;
|
||||
ps.scale = s;
|
||||
panel(dl, min, max, ps);
|
||||
|
||||
if (line.portrait) {
|
||||
ImVec2 p0 = snap(ImVec2(min.x + 20.0f * s, min.y + (boxH - portrait) * 0.5f)), p1 = p0 + ImVec2(portrait, portrait);
|
||||
dl->AddRectFilled(p0, p1, IM_COL32(4, 6, 12, 200), 3.0f * s);
|
||||
dl->AddImage(line.portrait, p0, p1, line.uv0, line.uv1);
|
||||
dl->AddRect(p0 - ImVec2(0.5f, 0.5f), p1 + ImVec2(0.5f, 0.5f), withAlpha(kGoldDim, 0.9f), 3.0f * s, 0,
|
||||
std::max(1.0f, s));
|
||||
}
|
||||
|
||||
// Typewriter: every line was wrapped from the full text, so words never jump.
|
||||
const char* shown = tb + revealed_;
|
||||
ImVec2 pos(min.x + textX, min.y + padY);
|
||||
for (const Span& sp : spans) {
|
||||
if (sp.b >= shown) break;
|
||||
drawSpan(dl, body, size, pos, kTextColor, sp.b, std::min(sp.e, shown), true);
|
||||
pos.y += lh;
|
||||
}
|
||||
|
||||
// Gold speaker name plate straddling the top-left edge.
|
||||
if (!line.speaker.empty()) {
|
||||
ImFont* nf = orDefault(g_fonts.name);
|
||||
const float ns = std::round(kNameSize * s);
|
||||
ImVec2 ts = measure(nf, ns, line.speaker);
|
||||
ImVec2 sz(std::round(ts.x + 44.0f * s), std::round(ns + 16.0f * s));
|
||||
ImVec2 n0 = snap(ImVec2(min.x + 46.0f * s, min.y - sz.y * 0.62f)), n1 = n0 + sz;
|
||||
const float r = 4.0f * s;
|
||||
for (int i = 4; i >= 1; --i) {
|
||||
float e = 2.0f * s * float(i);
|
||||
dl->AddRectFilled(n0 - ImVec2(e, e) + ImVec2(0, 3.0f * s), n1 + ImVec2(e, e) + ImVec2(0, 3.0f * s),
|
||||
IM_COL32(0, 0, 0, 22), r + e);
|
||||
}
|
||||
verticalGradient(dl, n0.y, n1.y, IM_COL32(246, 222, 160, 255), IM_COL32(170, 124, 56, 255),
|
||||
[&] { dl->AddRectFilled(n0, n1, IM_COL32_WHITE, r); });
|
||||
dl->AddRect(n0 + ImVec2(0.5f, 0.5f), n1 - ImVec2(0.5f, 0.5f), IM_COL32(92, 64, 26, 255), r, 0, std::max(1.0f, s));
|
||||
float in = std::round(3.0f * s);
|
||||
dl->AddRect(n0 + ImVec2(in, in) + ImVec2(0.5f, 0.5f), n1 - ImVec2(in, in) - ImVec2(0.5f, 0.5f),
|
||||
IM_COL32(255, 244, 210, 150), std::max(0.0f, r - in), 0, 1.0f);
|
||||
ImVec2 tp = snap(ImVec2(n0.x + (sz.x - ts.x) * 0.5f, n0.y + (sz.y - ts.y) * 0.5f));
|
||||
const char* sb = line.speaker.data();
|
||||
const char* se = sb + line.speaker.size();
|
||||
dl->AddText(nf, ns, tp + ImVec2(0, 1.0f), IM_COL32(255, 248, 225, 140), sb, se); // engraved highlight
|
||||
dl->AddText(nf, ns, tp, IM_COL32(40, 26, 10, 255), sb, se);
|
||||
}
|
||||
|
||||
if (choicesVisible()) {
|
||||
menuAt(dl, ImVec2(max.x - 28.0f * s, min.y - 14.0f * s), ImVec2(1.0f, 1.0f), choices_, selected_, {}, s * 0.9f);
|
||||
} else if (lineRevealed()) {
|
||||
advanceIndicator(dl, ImVec2(max.x - 34.0f * s, max.y - 26.0f * s), s, time_);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Toasts -------------------------------------------------------------------------------
|
||||
|
||||
void Toasts::push(std::string text, ImU32 accent, float seconds) {
|
||||
items_.push_back({std::move(text), accent, std::max(seconds, 0.6f)});
|
||||
if (items_.size() > 6) items_.erase(items_.begin());
|
||||
}
|
||||
|
||||
void Toasts::update(float dt) {
|
||||
for (Item& it : items_) it.age += dt;
|
||||
std::erase_if(items_, [](const Item& it) { return it.age >= it.seconds; });
|
||||
}
|
||||
|
||||
void Toasts::draw(ImVec2 viewport) {
|
||||
if (items_.empty()) return;
|
||||
ImDrawList* dl = ImGui::GetForegroundDrawList();
|
||||
const float s = scaleFor(viewport);
|
||||
ImFont* font = orDefault(g_fonts.small);
|
||||
const float size = std::round(kSmallSize * s);
|
||||
const float padX = std::round(28.0f * s), padY = std::round(12.0f * s), gemW = std::round(22.0f * s);
|
||||
float y = std::round(64.0f * s);
|
||||
for (const Item& it : items_) {
|
||||
float in = std::clamp(it.age / 0.25f, 0.0f, 1.0f), out = std::clamp((it.seconds - it.age) / 0.5f, 0.0f, 1.0f);
|
||||
float a = std::min(in, out);
|
||||
in = 1.0f - (1.0f - in) * (1.0f - in);
|
||||
ImVec2 ts = measure(font, size, it.text);
|
||||
ImVec2 sz(std::round(ts.x + padX * 2.0f + gemW), std::round(size + padY * 2.0f));
|
||||
ImVec2 min = snap(ImVec2((viewport.x - sz.x) * 0.5f, y - (1.0f - in) * 14.0f * s)), max = min + sz;
|
||||
PanelStyle ps;
|
||||
ps.scale = s;
|
||||
ps.ornate = false;
|
||||
ps.rounding = 4.0f;
|
||||
ps.alpha = 0.85f;
|
||||
ps.tint = withAlpha(IM_COL32_WHITE, a);
|
||||
panel(dl, min, max, ps);
|
||||
gem(dl, ImVec2(min.x + padX + 4.0f * s, (min.y + max.y) * 0.5f), 6.0f * s, it.accent,
|
||||
mix(it.accent, IM_COL32_WHITE, 0.55f), a);
|
||||
fadeLine(dl, ImVec2(min.x + 10.0f * s, max.y - 7.0f * s), ImVec2(max.x - 10.0f * s, max.y - 7.0f * s),
|
||||
std::max(1.0f, s), withAlpha(it.accent, 0.7f * a));
|
||||
text(dl, font, size, ImVec2(min.x + padX + gemW, min.y + padY - 1.0f * s), withAlpha(kTextColor, a), it.text);
|
||||
y += std::round((sz.y + 10.0f * s) * std::max(in, 0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace hd2d::ui
|
||||
@@ -16,103 +16,259 @@ Weather::Weather() {
|
||||
storm.horizon = {0.42f, 0.45f, 0.50f};
|
||||
storm.ground = {0.13f, 0.15f, 0.13f};
|
||||
storm.ambient = 0.62f;
|
||||
storm.clouds = 1.0f;
|
||||
storm.fogColor = {0.30f, 0.33f, 0.38f};
|
||||
storm.fogDensity = 0.03f;
|
||||
storm.fogHeightFalloff = 0.22f;
|
||||
storm.godRays = 0.6f;
|
||||
storm.shaftStrength = 0.2f;
|
||||
storm.cookieStrength = 0.0f;
|
||||
storm.shadowSoftness = 3.0f;
|
||||
storm.windStrength = 0.2f;
|
||||
storm.windGustSpeed = 3.0f;
|
||||
storm.bloomStrength = 0.06f;
|
||||
storm.saturation = 0.92f;
|
||||
|
||||
// Dry overcast: flat grey-white sky, weak diffuse sun with soft faint
|
||||
// shadows, muted colours.
|
||||
overcastLook.sunIntensity = 0.35f;
|
||||
overcastLook.sunColor = {0.88f, 0.89f, 0.92f};
|
||||
overcastLook.zenith = {0.56f, 0.59f, 0.63f};
|
||||
overcastLook.horizon = {0.72f, 0.73f, 0.75f};
|
||||
overcastLook.ground = {0.18f, 0.20f, 0.17f};
|
||||
overcastLook.ambient = 1.1f;
|
||||
overcastLook.clouds = 1.0f;
|
||||
overcastLook.fogColor = {0.46f, 0.48f, 0.52f};
|
||||
overcastLook.fogDensity = 0.016f;
|
||||
overcastLook.fogHeightFalloff = 0.15f;
|
||||
overcastLook.godRays = 0.8f;
|
||||
overcastLook.shaftStrength = 0.3f;
|
||||
overcastLook.cookieStrength = 0.0f;
|
||||
overcastLook.shadowSoftness = 5.0f;
|
||||
overcastLook.windStrength = 0.1f;
|
||||
overcastLook.windGustSpeed = 1.8f;
|
||||
overcastLook.bloomStrength = 0.05f;
|
||||
overcastLook.saturation = 0.85f;
|
||||
|
||||
// Morning mist: thick fog hugging the ground, sun still through it, few
|
||||
// god rays (the beams would drown in the dense fog), calm air.
|
||||
mist.sunIntensity = 2.4f;
|
||||
mist.sunColor = {1.0f, 0.92f, 0.82f};
|
||||
mist.zenith = {0.50f, 0.58f, 0.70f};
|
||||
mist.horizon = {0.80f, 0.82f, 0.84f};
|
||||
mist.ground = {0.20f, 0.22f, 0.18f};
|
||||
mist.ambient = 0.75f;
|
||||
mist.clouds = 0.3f;
|
||||
mist.fogColor = {0.70f, 0.72f, 0.76f};
|
||||
mist.fogDensity = 0.07f;
|
||||
mist.fogHeightFalloff = 0.5f;
|
||||
mist.godRays = 1.5f;
|
||||
mist.shaftStrength = 0.5f;
|
||||
mist.cookieStrength = 0.35f;
|
||||
mist.shadowSoftness = 2.5f;
|
||||
mist.windStrength = 0.03f;
|
||||
mist.windGustSpeed = 0.8f;
|
||||
mist.bloomStrength = 0.09f;
|
||||
mist.saturation = 0.9f;
|
||||
}
|
||||
|
||||
WeatherLook Weather::capture(const Scene& s, const RenderSettings& r) {
|
||||
WeatherLook l;
|
||||
l.sunIntensity = s.sun.intensity;
|
||||
l.sunColor = s.sun.color;
|
||||
l.zenith = s.sky.zenith;
|
||||
l.horizon = s.sky.horizon;
|
||||
l.ground = s.sky.ground;
|
||||
l.ambient = s.sky.ambient;
|
||||
l.fogColor = r.fogColor;
|
||||
l.fogDensity = r.fogDensity;
|
||||
l.fogHeightFalloff = r.fogHeightFalloff;
|
||||
l.godRays = r.godRayIntensity;
|
||||
l.shaftStrength = r.shaftStrength;
|
||||
l.cookieStrength = r.cookieStrength;
|
||||
l.windStrength = r.windStrength;
|
||||
l.windGustSpeed = r.windGustSpeed;
|
||||
l.bloomStrength = r.bloomStrength;
|
||||
l.saturation = r.saturation;
|
||||
return l;
|
||||
}
|
||||
|
||||
void Weather::apply(const WeatherLook& l, Scene& s, RenderSettings& r) {
|
||||
s.sun.intensity = l.sunIntensity;
|
||||
s.sun.color = l.sunColor;
|
||||
s.sky.zenith = l.zenith;
|
||||
s.sky.horizon = l.horizon;
|
||||
s.sky.ground = l.ground;
|
||||
s.sky.ambient = l.ambient;
|
||||
r.fogColor = l.fogColor;
|
||||
r.fogDensity = l.fogDensity;
|
||||
r.fogHeightFalloff = l.fogHeightFalloff;
|
||||
r.godRayIntensity = l.godRays;
|
||||
r.shaftStrength = l.shaftStrength;
|
||||
r.cookieStrength = l.cookieStrength;
|
||||
r.windStrength = l.windStrength;
|
||||
r.windGustSpeed = l.windGustSpeed;
|
||||
r.bloomStrength = l.bloomStrength;
|
||||
r.saturation = l.saturation;
|
||||
}
|
||||
|
||||
static WeatherLook mixLook(const WeatherLook& a, const WeatherLook& b, float t) {
|
||||
WeatherLook mixLooks(const WeatherLook& a, const WeatherLook& b, float t) {
|
||||
WeatherLook o;
|
||||
auto m = [t](auto x, auto y) { return x + (y - x) * t; };
|
||||
// Angles: blend along the shorter arc.
|
||||
float da = std::remainder(b.sunAzimuth - a.sunAzimuth, 6.2831853f);
|
||||
o.sunAzimuth = a.sunAzimuth + da * t;
|
||||
o.sunElevation = m(a.sunElevation, b.sunElevation);
|
||||
o.sunIntensity = m(a.sunIntensity, b.sunIntensity);
|
||||
o.sunColor = m(a.sunColor, b.sunColor);
|
||||
o.zenith = m(a.zenith, b.zenith);
|
||||
o.horizon = m(a.horizon, b.horizon);
|
||||
o.ground = m(a.ground, b.ground);
|
||||
o.ambient = m(a.ambient, b.ambient);
|
||||
o.fill = m(a.fill, b.fill);
|
||||
o.night = m(a.night, b.night);
|
||||
o.clouds = m(a.clouds, b.clouds);
|
||||
o.daylight = m(a.daylight, b.daylight);
|
||||
o.fogColor = m(a.fogColor, b.fogColor);
|
||||
o.fogDensity = m(a.fogDensity, b.fogDensity);
|
||||
o.fogHeightFalloff = m(a.fogHeightFalloff, b.fogHeightFalloff);
|
||||
o.godRays = m(a.godRays, b.godRays);
|
||||
o.shaftStrength = m(a.shaftStrength, b.shaftStrength);
|
||||
o.cookieStrength = m(a.cookieStrength, b.cookieStrength);
|
||||
o.shadowSoftness = m(a.shadowSoftness, b.shadowSoftness);
|
||||
o.windStrength = m(a.windStrength, b.windStrength);
|
||||
o.windGustSpeed = m(a.windGustSpeed, b.windGustSpeed);
|
||||
o.bloomStrength = m(a.bloomStrength, b.bloomStrength);
|
||||
o.saturation = m(a.saturation, b.saturation);
|
||||
o.exposure = m(a.exposure, b.exposure);
|
||||
o.lift = m(a.lift, b.lift);
|
||||
o.gain = m(a.gain, b.gain);
|
||||
o.fireflyGlow = m(a.fireflyGlow, b.fireflyGlow);
|
||||
return o;
|
||||
}
|
||||
|
||||
WeatherLook Weather::capture(const Scene& s, const RenderSettings& r) {
|
||||
WeatherLook l;
|
||||
l.sunAzimuth = s.sun.azimuth;
|
||||
l.sunElevation = s.sun.elevation;
|
||||
l.sunIntensity = s.sun.intensity;
|
||||
l.sunColor = s.sun.color;
|
||||
l.zenith = s.sky.zenith;
|
||||
l.horizon = s.sky.horizon;
|
||||
l.ground = s.sky.ground;
|
||||
l.ambient = s.sky.ambient;
|
||||
l.fill = s.sky.fill;
|
||||
l.night = s.sky.night;
|
||||
l.clouds = s.sky.clouds;
|
||||
l.daylight = 1.0f;
|
||||
l.fogColor = r.fogColor;
|
||||
l.fogDensity = r.fogDensity;
|
||||
l.fogHeightFalloff = r.fogHeightFalloff;
|
||||
l.godRays = r.godRayIntensity;
|
||||
l.shaftStrength = r.shaftStrength;
|
||||
l.cookieStrength = r.cookieStrength;
|
||||
l.shadowSoftness = r.shadowSoftness;
|
||||
l.windStrength = r.windStrength;
|
||||
l.windGustSpeed = r.windGustSpeed;
|
||||
l.bloomStrength = r.bloomStrength;
|
||||
l.saturation = r.saturation;
|
||||
l.exposure = r.exposure;
|
||||
l.lift = r.lift;
|
||||
l.gain = r.gain;
|
||||
l.fireflyGlow = s.particles ? s.particles->fireflyGlow : 1.0f;
|
||||
return l;
|
||||
}
|
||||
|
||||
void Weather::apply(const WeatherLook& l, Scene& s, RenderSettings& r) {
|
||||
s.sun.azimuth = l.sunAzimuth;
|
||||
s.sun.elevation = l.sunElevation;
|
||||
s.sun.intensity = l.sunIntensity;
|
||||
s.sun.color = l.sunColor;
|
||||
s.sky.zenith = l.zenith;
|
||||
s.sky.horizon = l.horizon;
|
||||
s.sky.ground = l.ground;
|
||||
s.sky.ambient = l.ambient;
|
||||
s.sky.fill = l.fill;
|
||||
s.sky.night = l.night;
|
||||
s.sky.clouds = l.clouds;
|
||||
r.fogColor = l.fogColor;
|
||||
r.fogDensity = l.fogDensity;
|
||||
r.fogHeightFalloff = l.fogHeightFalloff;
|
||||
r.godRayIntensity = l.godRays;
|
||||
r.shaftStrength = l.shaftStrength;
|
||||
r.cookieStrength = l.cookieStrength;
|
||||
r.shadowSoftness = l.shadowSoftness;
|
||||
r.windStrength = l.windStrength;
|
||||
r.windGustSpeed = l.windGustSpeed;
|
||||
r.bloomStrength = l.bloomStrength;
|
||||
r.saturation = l.saturation;
|
||||
r.exposure = l.exposure;
|
||||
r.lift = l.lift;
|
||||
r.gain = l.gain;
|
||||
if (s.particles) s.particles->fireflyGlow = l.fireflyGlow;
|
||||
}
|
||||
|
||||
void Weather::setClearLook(const WeatherLook& look) {
|
||||
clear_ = look;
|
||||
haveClear_ = externalClear_ = true;
|
||||
}
|
||||
|
||||
void Weather::snap() {
|
||||
rain_ = wetness_ = targetRain_;
|
||||
overcast_ = targetOvercast_;
|
||||
fog_ = targetFog_;
|
||||
}
|
||||
|
||||
bool Weather::clear() const {
|
||||
return rain_ <= 0.0f && targetRain_ <= 0.0f && overcast_ <= 0.0f && targetOvercast_ <= 0.0f && fog_ <= 0.0f &&
|
||||
targetFog_ <= 0.0f && flash_ <= 0.0f;
|
||||
}
|
||||
|
||||
// A preset placed in the clear look's time of day: darkened by its daylight,
|
||||
// tinted toward its sky hue by `hueKeep`, with the time-of-day fields copied.
|
||||
WeatherLook Weather::relative(const WeatherLook& preset, float hueKeep) const {
|
||||
const WeatherLook& c = clear_;
|
||||
const float b = glm::mix(nightBrightness, 1.0f, glm::clamp(c.daylight, 0.0f, 1.0f));
|
||||
WeatherLook o = preset;
|
||||
// The sun / moon fades faster than the sky (no moon glitter under storm
|
||||
// clouds), and weather never makes it brighter than the clear look.
|
||||
o.sunIntensity = std::min(preset.sunIntensity * b * b, c.sunIntensity);
|
||||
o.sunColor = glm::mix(preset.sunColor, c.sunColor, hueKeep);
|
||||
o.ambient = glm::mix(preset.ambient, c.ambient, hueKeep);
|
||||
o.zenith = glm::mix(preset.zenith * b, c.zenith, hueKeep);
|
||||
o.horizon = glm::mix(preset.horizon * b, c.horizon, hueKeep);
|
||||
o.ground = glm::mix(preset.ground * b, c.ground, hueKeep);
|
||||
o.fogColor = glm::mix(preset.fogColor * b, c.horizon, hueKeep);
|
||||
o.sunAzimuth = c.sunAzimuth;
|
||||
o.sunElevation = c.sunElevation;
|
||||
o.fill = c.fill;
|
||||
o.night = c.night;
|
||||
o.daylight = c.daylight;
|
||||
o.exposure = c.exposure;
|
||||
o.lift = c.lift;
|
||||
o.gain = c.gain;
|
||||
o.fireflyGlow = c.fireflyGlow;
|
||||
return o;
|
||||
}
|
||||
|
||||
static float smooth01(float x) { return x * x * (3.0f - 2.0f * x); }
|
||||
|
||||
static float approach(float v, float target, float step) {
|
||||
return v < target ? std::min(target, v + step) : std::max(target, v - step);
|
||||
}
|
||||
|
||||
void Weather::update(float dt, Scene& scene, RenderSettings& settings) {
|
||||
// While fully clear, keep tracking the live look so UI tweaks survive.
|
||||
if (rain_ <= 0.0f && targetRain_ <= 0.0f) {
|
||||
clear_ = capture(scene, settings);
|
||||
haveClear_ = true;
|
||||
} else if (!haveClear_) {
|
||||
// Without an external clear look, keep tracking the live look while fully
|
||||
// clear so UI tweaks survive.
|
||||
if (!externalClear_ && (clear() || !haveClear_)) {
|
||||
clear_ = capture(scene, settings);
|
||||
haveClear_ = true;
|
||||
}
|
||||
float step = dt / std::max(transitionSeconds, 1e-3f);
|
||||
rain_ = rain_ < targetRain_ ? std::min(targetRain_, rain_ + step) : std::max(targetRain_, rain_ - step);
|
||||
rain_ = approach(rain_, targetRain_, step);
|
||||
overcast_ = approach(overcast_, targetOvercast_, step);
|
||||
fog_ = approach(fog_, targetFog_, step);
|
||||
// Surfaces soak quickly and dry slowly.
|
||||
float wetRate = wetness_ < rain_ ? step * 1.5f : step * 0.3f;
|
||||
wetness_ = wetness_ < rain_ ? std::min(rain_, wetness_ + wetRate) : std::max(rain_, wetness_ - wetRate);
|
||||
wetness_ = approach(wetness_, rain_, wetness_ < rain_ ? step * 1.5f : step * 0.3f);
|
||||
|
||||
float k = rain_ * rain_ * (3.0f - 2.0f * rain_); // smoothstep
|
||||
if (rain_ > 0.0f || targetRain_ > 0.0f) apply(mixLook(clear_, storm, k), scene, settings);
|
||||
// Lightning: random double flashes while it pours.
|
||||
flashAge_ += dt;
|
||||
if (lightning_ && rain_ > 0.5f) {
|
||||
nextFlash_ -= dt * rain_;
|
||||
if (nextFlash_ <= 0.0f) {
|
||||
std::exponential_distribution<float> gap(1.0f / std::max(lightningInterval, 0.5f));
|
||||
nextFlash_ = 1.5f + gap(rng_);
|
||||
flashAge_ = 0.0f;
|
||||
}
|
||||
}
|
||||
// Two sharp pulses (0 and 0.18 s) with a quick decay.
|
||||
auto pulse = [](float t) { return t < 0.0f ? 0.0f : std::exp(-t * 18.0f); };
|
||||
flash_ = flashAge_ < 1.0f ? std::min(1.0f, pulse(flashAge_) + 0.7f * pulse(flashAge_ - 0.18f)) : 0.0f;
|
||||
|
||||
float kr = smooth01(rain_), ko = smooth01(overcast_), kf = smooth01(fog_);
|
||||
if (externalClear_ || !clear()) {
|
||||
const float night = 1.0f - glm::clamp(clear_.daylight, 0.0f, 1.0f);
|
||||
WeatherLook l = clear_;
|
||||
if (ko > 0.0f) l = mixLooks(l, relative(overcastLook, 0.5f * night), ko);
|
||||
if (kf > 0.0f) l = mixLooks(l, relative(mist, 0.45f + 0.3f * night), kf);
|
||||
if (kr > 0.0f) l = mixLooks(l, relative(storm, 0.5f * night), kr);
|
||||
if (flash_ > 0.0f) {
|
||||
const glm::vec3 bolt(0.75f, 0.8f, 1.0f);
|
||||
l.sunIntensity += 7.0f * flash_;
|
||||
l.sunColor = glm::mix(l.sunColor, bolt, flash_);
|
||||
l.zenith += bolt * 1.4f * flash_;
|
||||
l.horizon += bolt * 1.6f * flash_;
|
||||
l.fogColor += bolt * 0.8f * flash_;
|
||||
l.ambient += 0.6f * flash_;
|
||||
}
|
||||
apply(l, scene, settings);
|
||||
}
|
||||
scene.rain = rain_;
|
||||
scene.wetness = wetness_;
|
||||
|
||||
if (scene.particles) {
|
||||
ParticleSystem& ps = *scene.particles;
|
||||
ps.ambientFade = 1.0f - k;
|
||||
ps.ambientFade = (1.0f - kr) * (1.0f - 0.6f * kf);
|
||||
glm::vec2 wd = glm::length(settings.windDirection) > 1e-4f ? glm::normalize(settings.windDirection)
|
||||
: glm::vec2(1, 0);
|
||||
ps.wind = glm::vec3(wd.x, 0.0f, wd.y) * settings.windStrength * 10.0f;
|
||||
@@ -120,7 +276,9 @@ void Weather::update(float dt, Scene& scene, RenderSettings& settings) {
|
||||
// Ground splashes around the focus point (water gets ripples instead).
|
||||
splashAccum_ += splashesPerSecond * rain_ * dt;
|
||||
std::uniform_real_distribution<float> u(-1.0f, 1.0f);
|
||||
glm::vec3 color = (scene.sky.horizon * 0.8f + glm::vec3(0.15f)) * scene.sky.ambient * 1.6f;
|
||||
// Splash highlight dims with the sky (no bright specks at night).
|
||||
float lit = glm::clamp(scene.sky.horizon.g / 0.45f, 0.0f, 1.0f);
|
||||
glm::vec3 color = (scene.sky.horizon * 0.8f + glm::vec3(0.15f * lit)) * scene.sky.ambient * 1.6f;
|
||||
while (splashAccum_ >= 1.0f) {
|
||||
splashAccum_ -= 1.0f;
|
||||
glm::vec2 p = glm::vec2(scene.focusPoint.x, scene.focusPoint.z) + glm::vec2(u(rng_), u(rng_)) * splashRadius;
|
||||
|
||||
@@ -17,7 +17,8 @@ static void APIENTRY glDebugCallback(GLenum, GLenum type, GLuint, GLenum severit
|
||||
HD2D_ERR("GL: %s", message);
|
||||
}
|
||||
|
||||
bool Window::create(int width, int height, const std::string& title, bool vsync, bool resizable) {
|
||||
bool Window::create(int width, int height, const std::string& title, bool vsync, bool resizable, bool visible) {
|
||||
visible_ = visible;
|
||||
glfwSetErrorCallback([](int code, const char* msg) { HD2D_ERR("GLFW %d: %s", code, msg); });
|
||||
if (!glfwInit()) return false;
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
|
||||
@@ -25,6 +26,9 @@ bool Window::create(int width, int height, const std::string& title, bool vsync,
|
||||
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
|
||||
glfwWindowHint(GLFW_RESIZABLE, resizable ? GLFW_TRUE : GLFW_FALSE);
|
||||
glfwWindowHint(GLFW_VISIBLE, visible ? GLFW_TRUE : GLFW_FALSE);
|
||||
glfwWindowHint(GLFW_FOCUSED, visible ? GLFW_TRUE : GLFW_FALSE);
|
||||
glfwWindowHint(GLFW_FOCUS_ON_SHOW, visible ? GLFW_TRUE : GLFW_FALSE);
|
||||
#ifndef NDEBUG
|
||||
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
|
||||
#endif
|
||||
@@ -34,7 +38,7 @@ bool Window::create(int width, int height, const std::string& title, bool vsync,
|
||||
return false;
|
||||
}
|
||||
glfwMakeContextCurrent(window_);
|
||||
glfwSwapInterval(vsync ? 1 : 0);
|
||||
glfwSwapInterval(vsync && visible ? 1 : 0);
|
||||
glfwSetWindowUserPointer(window_, this);
|
||||
glfwSetFramebufferSizeCallback(window_, onFramebufferSize);
|
||||
glfwSetScrollCallback(window_, onScroll);
|
||||
@@ -75,11 +79,13 @@ bool Window::pollEvents() {
|
||||
return !glfwWindowShouldClose(window_);
|
||||
}
|
||||
|
||||
void Window::swapBuffers() { glfwSwapBuffers(window_); }
|
||||
void Window::swapBuffers() {
|
||||
if (visible_) glfwSwapBuffers(window_);
|
||||
}
|
||||
|
||||
bool Window::keyDown(int key) const { return key >= 0 && key < 512 && keys_[key]; }
|
||||
bool Window::keyDown(int key) const { return visible_ && key >= 0 && key < 512 && keys_[key]; }
|
||||
bool Window::keyPressed(int key) const { return keyDown(key) && !prevKeys_[key]; }
|
||||
bool Window::mouseDown(int button) const { return glfwGetMouseButton(window_, button) == GLFW_PRESS; }
|
||||
bool Window::mouseDown(int button) const { return visible_ && glfwGetMouseButton(window_, button) == GLFW_PRESS; }
|
||||
void Window::setTitle(const std::string& title) { glfwSetWindowTitle(window_, title.c_str()); }
|
||||
void Window::requestClose() { glfwSetWindowShouldClose(window_, GLFW_TRUE); }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user