This commit is contained in:
Gabrieal Jimmy
2026-10-02 12:42:00 -05:00
commit 2c99305fb0
145 changed files with 6525 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
#pragma once
#include "hd2d/GL.h"
#include "hd2d/Texture.h"
#include <glm/glm.hpp>
#include <vector>
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.
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)
};
// Instanced batch of billboards sharing one texture.
class BillboardBatch {
public:
BillboardBatch() = default;
BillboardBatch(const BillboardBatch&) = delete;
BillboardBatch& operator=(const BillboardBatch&) = delete;
~BillboardBatch();
bool loadTexture(const std::string& path) { return texture.load(path); }
void upload(); // call after editing `instances`
void draw() const;
// World size that keeps the texture at `pixelsPerUnit` texel density.
glm::vec2 sizeForDensity(float pixelsPerUnit) const {
return glm::vec2(texture.size()) / pixelsPerUnit;
}
Texture texture;
std::vector<BillboardInstance> instances;
bool castShadows = true;
bool inReflections = true;
float emissive = 0.0f; // unlit contribution, for glowing sprites
private:
GLuint vao_ = 0, vbo_ = 0;
size_t capacity_ = 0;
GLsizei count_ = 0;
};
} // namespace hd2d

View File

@@ -0,0 +1,24 @@
#pragma once
#include <glm/glm.hpp>
namespace hd2d {
// Orbit camera looking at `target`. HD-2D scenes use a high, narrow-FOV
// (near telephoto) camera tilted down so the world reads like a diorama.
struct Camera {
glm::vec3 target{0.0f};
float yaw = 0.0f; // radians; 0 looks toward -Z
float pitch = 0.56f; // radians below horizon
float distance = 17.0f;
float fovY = 0.52f; // radians (~30 deg)
float nearPlane = 0.5f;
float farPlane = 250.0f;
glm::vec3 forward() const; // unit vector camera -> target
glm::vec3 position() const { return target - forward() * distance; }
glm::vec3 right() const; // horizontal right vector
glm::mat4 view() const;
glm::mat4 projection(float aspect) const;
};
} // namespace hd2d

View File

@@ -0,0 +1,29 @@
#pragma once
namespace hd2d {
class Window;
struct RenderSettings;
struct Scene;
class Weather;
// Dear ImGui integration plus a ready-made tuning panel for the renderer.
class DebugUI {
public:
bool init(Window& window);
void shutdown();
void beginFrame();
void endFrame();
bool wantsMouse() const;
bool wantsKeyboard() const;
// 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);
private:
bool initialized_ = false;
};
} // namespace hd2d

4
engine/include/hd2d/GL.h Normal file
View File

@@ -0,0 +1,4 @@
#pragma once
// Single include point for OpenGL. libepoxy resolves entry points for both
// GLX (X11) and EGL (Wayland) contexts, so no loader init call is needed.
#include <epoxy/gl.h>

View File

@@ -0,0 +1,35 @@
#pragma once
#include "hd2d/GL.h"
#include <glm/glm.hpp>
#include <vector>
namespace hd2d {
class ModelBatch;
// Baked contact ambient occlusion on the ground under 3D scenery: a dark,
// tight core at a tree trunk / bush / rock base plus a wide, soft canopy
// shadow under tall models. Sampled by terrain, sprite and model shaders, so
// grass tufts and bushes growing at a tree's foot darken too. Rebuild after
// moving instances.
class GroundOcclusion {
public:
GroundOcclusion() = default;
GroundOcclusion(const GroundOcclusion&) = delete;
GroundOcclusion& operator=(const GroundOcclusion&) = delete;
~GroundOcclusion();
// Covers the rectangle [min, max] (world xz). Uses every batch whose
// `groundOcclusion` strength is > 0.
void build(glm::vec2 min, glm::vec2 max, const std::vector<ModelBatch*>& batches, int texelsPerUnit = 8);
GLuint texture() const { return tex_; }
glm::vec4 rect() const { return rect_; } // matches uGroundAO in common.glsl
private:
GLuint tex_ = 0;
glm::vec4 rect_{0.0f};
};
} // namespace hd2d

View File

@@ -0,0 +1,5 @@
#pragma once
#include <cstdio>
#define HD2D_LOG(fmt, ...) std::fprintf(stdout, "[hd2d] " fmt "\n" __VA_OPT__(, ) __VA_ARGS__)
#define HD2D_ERR(fmt, ...) std::fprintf(stderr, "[hd2d:error] " fmt "\n" __VA_OPT__(, ) __VA_ARGS__)

106
engine/include/hd2d/Model.h Normal file
View File

@@ -0,0 +1,106 @@
#pragma once
#include "hd2d/GL.h"
#include "hd2d/Texture.h"
#include <glm/glm.hpp>
#include <cmath>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace hd2d {
// Material of a glTF primitive. Materials whose name starts with "foliage"
// get alpha testing, two-sided wrap lighting and sun translucency.
struct ModelMaterial {
std::string name;
const Texture* texture = nullptr; // base color (sRGB); null = white
glm::vec4 baseColor{1.0f};
bool alphaMask = false;
float alphaCutoff = 0.5f;
bool doubleSided = false;
bool foliage = false;
glm::vec3 emissive{0.0f}; // linear HDR (factor x strength)
const Texture* emissiveTexture = nullptr; // multiplies `emissive`; null = white
};
struct ModelPrimitive {
GLuint vbo = 0, ibo = 0;
GLsizei indexCount = 0;
const ModelMaterial* material = nullptr; // owned by the ModelLibrary
};
// Static mesh made of primitives (one per material). Vertex layout: position,
// normal, uv, color (rgb = tint/AO, a = wind weight). Built from one root
// node of a glTF file with the node's world transform baked in.
struct Model {
std::string name;
std::vector<ModelPrimitive> primitives;
glm::vec3 boundsMin{0.0f}, boundsMax{0.0f};
// Named attachment points from child empties of the glTF node (model space),
// e.g. "fire_0" on the bridge braziers.
std::unordered_map<std::string, glm::vec3> sockets;
};
// Loads a .glb/.gltf once (textures shared by all its models) and exposes each
// root node with a mesh as a named Model.
class ModelLibrary {
public:
ModelLibrary() = default;
ModelLibrary(const ModelLibrary&) = delete;
ModelLibrary& operator=(const ModelLibrary&) = delete;
~ModelLibrary();
bool load(const std::string& path);
const Model* get(const std::string& name) const;
private:
std::vector<std::unique_ptr<Texture>> textures_;
std::vector<std::unique_ptr<ModelMaterial>> materials_;
std::unordered_map<std::string, std::unique_ptr<Model>> models_;
};
// Per-instance placement: rotation about +Y, non-uniform scale, brightness tint.
struct ModelInstance {
glm::vec3 pos{0.0f};
float yaw = 0.0f;
glm::vec3 scale{1.0f};
float tint = 1.0f;
glm::vec3 toWorld(const glm::vec3& local) const { // matches mesh.vert
glm::vec3 p = local * scale;
float c = std::cos(yaw), s = std::sin(yaw);
return pos + glm::vec3(c * p.x + s * p.z, p.y, -s * p.x + c * p.z);
}
};
// Instanced draw of one model.
class ModelBatch {
public:
explicit ModelBatch(const Model* model = nullptr) : model(model) {}
ModelBatch(const ModelBatch&) = delete;
ModelBatch& operator=(const ModelBatch&) = delete;
~ModelBatch();
void upload(); // call after editing `instances`
// Draws primitive `i` (the renderer binds its material first).
void drawPrimitive(size_t i) const;
const Model* model = nullptr;
std::vector<ModelInstance> instances;
bool castShadows = true;
bool inReflections = true;
float windScale = 1.0f; // multiplies the per-vertex wind weight
// Strength of the contact shadow baked into the ground under each instance
// (see GroundOcclusion); 0 disables. Its size follows the model's footprint.
float groundOcclusion = 0.0f;
private:
std::vector<GLuint> vaos_;
GLuint instanceVbo_ = 0;
size_t capacity_ = 0;
GLsizei count_ = 0;
};
} // namespace hd2d

View File

@@ -0,0 +1,85 @@
#pragma once
#include "hd2d/GL.h"
#include <glm/glm.hpp>
#include <random>
#include <vector>
namespace hd2d {
struct PointLight;
// High-resolution (non pixel-art) particles: ambient fields (fireflies, dust),
// fire emitters (flames, embers, smoke, glow + flickering light) and bursts.
class ParticleSystem {
public:
// Must match the kind ids in particles.vert / particles.frag.
enum class Kind : int {
Firefly = 0, // blinking emissive glow, wanders near the ground
Mote = 1, // dust speck only visible where sunlight hits it (god rays)
Spark = 2, // ballistic burst particle (footstep dust, rain splashes, ...)
Flame = 3, // additive tongue of fire, eroded by noise as it ages
Ember = 4, // tiny hot spark drifting up on turbulent air
Smoke = 5, // alpha-blended puff rising above a fire
Glow = 6, // large soft halo around a fire (one per emitter per frame)
};
struct Field { // region that keeps a constant population of ambient particles
Kind kind;
glm::vec3 min, max;
int count;
};
struct Fire {
glm::vec3 pos; // base of the flames
float scale = 1.0f; // overall size / intensity
float flameAccum = 0.0f, emberAccum = 0.0f, smokeAccum = 0.0f;
float flicker = 1.0f; // current light flicker multiplier
float phase = 0.0f;
};
~ParticleSystem();
void addField(Kind kind, glm::vec3 min, glm::vec3 max, int count);
int addFire(glm::vec3 pos, float scale = 1.0f);
void burst(glm::vec3 pos, int count, glm::vec3 color, float speed, float size, float life);
void update(float dt, float time);
void draw() const;
// Appends fire lights first, then up to `maxFireflies` firefly lights.
void collectLights(std::vector<PointLight>& out, int maxFireflies) const;
const std::vector<Fire>& fires() const { return fires_; }
glm::vec3 fireflyColor{2.6f, 3.2f, 0.9f};
glm::vec3 moteColor{1.0f, 0.95f, 0.8f};
float fireflyLightRadius = 2.2f;
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)
private:
struct Particle {
glm::vec3 pos, vel;
glm::vec3 color;
float size, age, life, phase;
Kind kind;
int field; // ambient field index, or -1
int fire; // owning fire, or -1
};
struct GpuParticle {
glm::vec4 posSize;
glm::vec4 color; // rgb, a = alpha
glm::vec4 params; // x = kind, y = age / life, z = random seed
};
void spawnInField(Particle& p, int fieldIndex, bool randomAge);
void spawnFire(const Fire& f, int fireIndex, Kind kind);
std::vector<Field> fields_;
std::vector<Fire> fires_;
std::vector<Particle> particles_;
std::vector<GpuParticle> gpu_;
std::mt19937 rng_{1337};
GLuint vao_ = 0, vbo_ = 0;
size_t capacity_ = 0;
};
} // namespace hd2d

View File

@@ -0,0 +1,40 @@
#pragma once
#include "hd2d/GL.h"
#include <initializer_list>
#include <vector>
namespace hd2d {
// Creates an immutable 2D texture with clamp-to-edge sampling.
GLuint createTexture2D(GLenum internalFormat, int w, int h, GLenum filter = GL_LINEAR, int levels = 1);
// Framebuffer owning its color attachments. Depth can be owned (depthFormat)
// or borrowed from another target (sharedDepth) so passes can depth-test
// against an earlier pass without copying.
class RenderTarget {
public:
RenderTarget() = default;
RenderTarget(const RenderTarget&) = delete;
RenderTarget& operator=(const RenderTarget&) = delete;
~RenderTarget() { destroy(); }
void create(int w, int h, std::initializer_list<GLenum> colorFormats, GLenum depthFormat = 0,
GLuint sharedDepth = 0, GLenum filter = GL_LINEAR);
void destroy();
void bind() const; // binds FBO and sets viewport
GLuint fbo() const { return fbo_; }
GLuint color(int i = 0) const { return colors_[i]; }
GLuint depth() const { return depth_; }
int width() const { return w_; }
int height() const { return h_; }
private:
GLuint fbo_ = 0, depth_ = 0;
bool ownsDepth_ = false;
std::vector<GLuint> colors_;
int w_ = 0, h_ = 0;
};
} // namespace hd2d

View File

@@ -0,0 +1,100 @@
#pragma once
#include "hd2d/Camera.h"
#include "hd2d/RenderTarget.h"
#include "hd2d/Scene.h"
#include "hd2d/Shader.h"
#include <glm/glm.hpp>
#include <memory>
#include <string>
namespace hd2d {
// All tunable look parameters of the HD-2D pipeline.
struct RenderSettings {
// Shadows
int shadowMapSize = 4096;
float shadowExtent = 34.0f; // half-size of the sun's ortho frustum, world units
float shadowSoftness = 1.5f; // PCF radius in texels
// Dappled canopy light ("cookie" projected along the sun direction)
float cookieStrength = 0.55f;
float cookieScale = 0.13f;
float cookieSpeed = 0.05f;
float cookieCoverage = 0.48f; // fraction of light blocked
// God ray beams: the same implied canopy, sparser, applied inside the fog
float shaftStrength = 0.9f; // 1 = sun scatters only inside beams
float shaftCoverage = 0.62f; // higher = fewer, narrower beams
float shaftScale = 0.45f; // beam pattern frequency (larger = thinner beams)
// Volumetric fog + god rays
bool volumetrics = true;
glm::vec3 fogColor{0.30f, 0.36f, 0.45f}; // ambient fog in-scatter tint
float fogDensity = 0.008f;
float fogHeightFalloff = 0.15f;
float fogBaseHeight = 0.0f;
float fogNoise = 0.6f;
float fogAnisotropy = 0.62f; // Henyey-Greenstein g (forward scattering)
float godRayIntensity = 4.5f;
float fogMaxDistance = 90.0f;
int fogSteps = 56;
// Wind for foliage
glm::vec2 windDirection{0.8f, 0.35f};
float windStrength = 0.06f;
float windGustSpeed = 1.2f;
// Depth of field (tilt-shift miniature look)
bool dof = true;
float focusRange = 6.0f; // fully sharp half-width around the focus distance
float focusTransition = 12.0f; // distance over which blur ramps up
float dofMaxRadius = 9.0f; // max blur radius in half-res pixels
float tiltShift = 0.35f; // extra blur toward screen top/bottom
// Bloom
bool bloom = true;
float bloomStrength = 0.075f;
float bloomRadius = 1.0f;
// Color grading
float exposure = 1.0f;
float contrast = 1.08f;
float saturation = 1.12f;
glm::vec3 lift{0.012f, 0.010f, 0.025f}; // added to shadows
glm::vec3 gain{1.04f, 1.00f, 0.93f}; // multiplied into highlights
float vignette = 0.42f;
float chromaticAberration = 0.0015f;
float grain = 0.025f;
bool reflections = true;
bool occluderFade = true; // dither away 3D scenery between the camera and the player
// Screen-space ambient occlusion (contact darkening everywhere; the baked
// GroundOcclusion handles the large soft shade under trees)
bool ssao = true;
bool groundOcclusion = true; // use Scene::groundOcclusion (baked contact AO)
float ssaoRadius = 0.7f; // world units
float ssaoIntensity = 1.6f;
int maxRainDrops = 18000; // streaks drawn at Scene::rain = 1
};
class Renderer {
public:
Renderer();
~Renderer();
bool init(const std::string& shaderDir, int width, int height);
void resize(int width, int height);
bool reloadShaders();
void render(const Scene& scene, const Camera& camera, float time);
RenderSettings settings;
float gpuFrameMs() const { return gpuMs_; }
private:
struct Impl;
std::unique_ptr<Impl> impl_;
float gpuMs_ = 0.0f;
};
} // namespace hd2d

View File

@@ -0,0 +1,60 @@
#pragma once
#include "hd2d/Billboard.h"
#include "hd2d/GroundOcclusion.h"
#include "hd2d/Model.h"
#include "hd2d/Particles.h"
#include "hd2d/Terrain.h"
#include "hd2d/Water.h"
#include <glm/glm.hpp>
#include <vector>
namespace hd2d {
struct PointLight {
glm::vec3 position{0.0f};
float radius = 3.0f;
glm::vec3 color{1.0f}; // linear HDR
};
struct Sun {
float azimuth = -0.6f; // radians, rotation around Y (0 = toward -Z, the default view direction)
float elevation = 0.66f; // radians above horizon
glm::vec3 color{1.0f, 0.86f, 0.66f};
float intensity = 3.2f;
glm::vec3 direction() const; // unit vector pointing toward the sun
};
struct Sky {
glm::vec3 zenith{0.22f, 0.42f, 0.78f};
glm::vec3 horizon{0.70f, 0.76f, 0.80f};
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
};
// Everything the renderer draws. Pointers are non-owning; null entries skip.
struct Scene {
Terrain* terrain = nullptr;
Water* water = nullptr;
ParticleSystem* particles = nullptr;
std::vector<BillboardBatch*> billboards; // pixel-art sprites (characters, small props)
std::vector<ModelBatch*> models; // 3D scenery (trees, bushes, rocks, bridge)
const GroundOcclusion* groundOcclusion = nullptr; // baked contact AO under scenery
std::vector<PointLight> lights;
int volumetricLights = 0; // the first N lights also scatter in the fog (e.g. fires)
Sun sun;
Sky sky;
glm::vec3 focusPoint{0.0f}; // depth-of-field focus (usually the player)
glm::vec3 interactor{0.0f}; // pushes foliage aside (usually the player)
float interactorRadius = 1.1f;
glm::vec3 blobShadowPos{0.0f}; // soft contact shadow under the player
float blobShadowRadius = 0.0f; // 0 disables
// Precipitation, usually driven by Weather.
float rain = 0.0f; // 0..1: rain streaks, water ripples, overcast sky
float wetness = 0.0f; // 0..1: darker, glossy surfaces (lags behind `rain`)
float rainSpeed = 15.0f; // drop fall speed (world units / s)
};
} // namespace hd2d

View File

@@ -0,0 +1,9 @@
#pragma once
#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);
} // namespace hd2d

View File

@@ -0,0 +1,42 @@
#pragma once
#include "hd2d/GL.h"
#include <glm/glm.hpp>
#include <string>
#include <unordered_map>
namespace hd2d {
// GLSL program loaded from files. Supports `#include "file.glsl"` (resolved
// relative to the including file) and hot reload: a failed reload keeps the
// previous working program.
class Shader {
public:
Shader() = default;
Shader(const Shader&) = delete;
Shader& operator=(const Shader&) = delete;
~Shader();
bool load(const std::string& vertPath, const std::string& fragPath);
bool reload();
void use() const { glUseProgram(program_); }
GLuint id() const { return program_; }
void set(const char* name, int v);
void set(const char* name, float v);
void set(const char* name, const glm::vec2& v);
void set(const char* name, const glm::vec3& v);
void set(const char* name, const glm::vec4& v);
void set(const char* name, const glm::ivec2& v);
void set(const char* name, const glm::ivec4& v);
void set(const char* name, const glm::mat4& v);
private:
GLint location(const char* name);
GLuint program_ = 0;
std::string vertPath_, fragPath_;
std::unordered_map<std::string, GLint> locations_;
};
} // namespace hd2d

View File

@@ -0,0 +1,61 @@
#pragma once
#include "hd2d/GL.h"
#include "hd2d/Texture.h"
#include <array>
#include <cstdint>
#include <glm/glm.hpp>
#include <vector>
namespace hd2d {
// How a material id maps onto layers of the terrain texture array.
struct TerrainMaterial {
int topFirst = 0, topCount = 1; // layers picked per tile for top faces
int accentFirst = 0, accentCount = 0; // occasional variation layers
float accentChance = 0.0f; // probability a tile uses an accent layer
int sideFirst = 0, sideCount = 1; // layers for cliff faces below this tile
int lipLayer = -1; // layer for an overhang strip atop cliff faces (-1 = none)
};
// Tile-based diorama terrain: every tile has an integer height level and a
// material. Top faces are drawn with organically jittered material borders
// at pixel granularity; height differences produce textured cliff faces.
class Terrain {
public:
static constexpr int kMaxMaterials = 8;
void init(int width, int depth, float levelHeight = 0.5f);
int width() const { return w_; }
int depth() const { return d_; }
float levelHeight() const { return levelHeight_; }
bool inBounds(int x, int z) const { return x >= 0 && z >= 0 && x < w_ && z < d_; }
int level(int x, int z) const;
uint8_t material(int x, int z) const;
void setLevel(int x, int z, int level);
void setMaterial(int x, int z, uint8_t mat);
float heightAt(float x, float z) const; // top surface height of tile under (x, z)
// Cliff faces at the map border extend down to this level (diorama base).
int baseLevel = -6;
std::array<TerrainMaterial, kMaxMaterials> materials{};
// (Re)builds GPU mesh + material map after edits.
void build();
void draw() const;
void bindMaterialMap(GLuint unit) const { glBindTextureUnit(unit, matTex_); }
TextureArray textures; // pixel-art material layers, all same size
private:
int w_ = 0, d_ = 0;
float levelHeight_ = 0.5f;
std::vector<int> levels_;
std::vector<uint8_t> mats_;
GLuint vao_ = 0, vbo_ = 0, ibo_ = 0, matTex_ = 0;
GLsizei indexCount_ = 0;
};
} // namespace hd2d

View File

@@ -0,0 +1,68 @@
#pragma once
#include "hd2d/GL.h"
#include <glm/glm.hpp>
#include <string>
#include <vector>
namespace hd2d {
struct Image {
int width = 0, height = 0;
std::vector<unsigned char> rgba; // row 0 = bottom (GL convention)
};
// Loads an RGBA8 image flipped to GL orientation. Colors of fully transparent
// pixels are replaced with their nearest opaque neighbour so that filtering
// never bleeds dark fringes into sprite edges.
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);
// sRGB pixel-art texture. Sampled with "sharp bilinear" in shaders, so it uses
// linear filtering with mipmaps rather than GL_NEAREST.
class Texture {
public:
Texture() = default;
Texture(const Texture&) = delete;
Texture& operator=(const Texture&) = delete;
Texture(Texture&& o) noexcept { *this = std::move(o); }
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);
void bind(GLuint unit) const { glBindTextureUnit(unit, id_); }
GLuint id() const { return id_; }
glm::ivec2 size() const { return {width_, height_}; }
// UV-space rectangle (minX, minY, maxX, maxY) of pixels with alpha >= 128.
// Billboards use minY to plant the visible feet on the ground.
glm::vec4 opaqueBounds() const { return opaqueBounds_; }
private:
GLuint id_ = 0;
int width_ = 0, height_ = 0;
glm::vec4 opaqueBounds_{0, 0, 1, 1};
};
// 2D array of equally sized sRGB layers (terrain materials).
class TextureArray {
public:
TextureArray() = default;
TextureArray(const TextureArray&) = delete;
TextureArray& operator=(const TextureArray&) = delete;
~TextureArray();
bool load(const std::vector<std::string>& paths);
void bind(GLuint unit) const { glBindTextureUnit(unit, id_); }
glm::ivec2 size() const { return {width_, height_}; }
int layers() const { return layers_; }
private:
GLuint id_ = 0;
int width_ = 0, height_ = 0, layers_ = 0;
};
} // namespace hd2d

View File

@@ -0,0 +1,50 @@
#pragma once
#include "hd2d/GL.h"
#include <glm/glm.hpp>
namespace hd2d {
class Terrain;
// Planar water body: reflective, refractive, depth-absorbing surface with
// procedural waves, sun glints and shoreline foam. Rendered at full
// resolution (not pixelated) on purpose, like HD-2D's water.
class Water {
public:
~Water();
// Builds a tessellated grid covering [minXZ, maxXZ] at height `level`.
void build(glm::vec2 minXZ, glm::vec2 maxXZ, float level, float cellSize = 0.25f);
// Distance-to-shore field (world units) over the water bounds, from every
// terrain tile whose top is above the water. Drives foam and ripples.
void buildShoreField(const Terrain& terrain, int texelsPerUnit = 8);
void draw() const;
float level() const { return level_; }
GLuint shoreTexture() const { return shoreTex_; }
// (minX, minZ, 1/sizeX, 1/sizeZ): maps world xz to shore texture uv.
glm::vec4 shoreRect() const { return {min_.x, min_.y, 1.0f / (max_.x - min_.x), 1.0f / (max_.y - min_.y)}; }
// Look parameters (tweakable at runtime).
glm::vec3 shallowColor{0.03f, 0.40f, 0.62f};
glm::vec3 deepColor{0.005f, 0.10f, 0.30f};
glm::vec3 absorption{1.6f, 0.5f, 0.22f}; // per unit of light path, per channel
float clarity = 0.5f; // how much of the bottom stays visible
float waveScale = 0.9f;
float waveStrength = 1.2f;
float waveSpeed = 0.35f;
float refraction = 0.035f;
float reflectionDistort = 0.05f;
float specular = 6.0f;
float glitter = 1.0f; // twinkling point sparkles
float foamWidth = 0.45f; // world units from the shore
float foamIntensity = 0.85f;
float causticsIntensity = 1.4f;
private:
GLuint vao_ = 0, vbo_ = 0, ibo_ = 0, shoreTex_ = 0;
GLsizei indexCount_ = 0;
float level_ = 0.0f;
glm::vec2 min_{0.0f}, max_{1.0f};
};
} // namespace hd2d

View File

@@ -0,0 +1,54 @@
#pragma once
#include "hd2d/Scene.h"
#include <glm/glm.hpp>
#include <random>
namespace hd2d {
struct RenderSettings;
// Everything the weather changes about the look, so it can be blended.
struct WeatherLook {
float sunIntensity = 0.0f;
glm::vec3 sunColor{1.0f};
glm::vec3 zenith{0.5f}, horizon{0.5f}, ground{0.2f};
float ambient = 0.5f;
glm::vec3 fogColor{0.5f};
float fogDensity = 0.0f, fogHeightFalloff = 0.1f, godRays = 0.0f, shaftStrength = 0.0f, cookieStrength = 0.0f;
float windStrength = 0.0f, windGustSpeed = 0.0f;
float bloomStrength = 0.0f, saturation = 1.0f;
};
// 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.
class Weather {
public:
Weather();
void setRain(float target) { targetRain_ = glm::clamp(target, 0.0f, 1.0f); }
float targetRain() const { return targetRain_; }
float rain() const { return rain_; }
// Jumps straight to the target (no transition), e.g. for screenshots.
void snap() { rain_ = targetRain_; wetness_ = targetRain_; }
void update(float dt, Scene& scene, RenderSettings& settings);
WeatherLook storm; // look at full rain intensity
float transitionSeconds = 5.0f; // clear <-> storm fade time
float splashesPerSecond = 500.0f;
float splashRadius = 12.0f; // around Scene::focusPoint
private:
static WeatherLook capture(const Scene& scene, const RenderSettings& settings);
static void apply(const WeatherLook& look, Scene& scene, RenderSettings& settings);
WeatherLook clear_;
bool haveClear_ = false;
float rain_ = 0.0f, targetRain_ = 0.0f, wetness_ = 0.0f, splashAccum_ = 0.0f;
std::mt19937 rng_{99};
};
} // namespace hd2d

View File

@@ -0,0 +1,49 @@
#pragma once
#include <glm/vec2.hpp>
#include <string>
struct GLFWwindow;
namespace hd2d {
// GLFW window + OpenGL 4.5 core context with simple polled input.
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);
void destroy();
// Polls events and refreshes per-frame input state. Returns false once the
// window should close.
bool pollEvents();
void swapBuffers();
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_; }
int width() const { return width_; }
int height() const { return height_; }
bool resized() const { return resized_; }
GLFWwindow* handle() const { return window_; }
void setTitle(const std::string& title);
void requestClose();
private:
static void onFramebufferSize(GLFWwindow* w, int width, int height);
static void onScroll(GLFWwindow* w, double dx, double dy);
GLFWwindow* window_ = nullptr;
int width_ = 0, height_ = 0;
bool resized_ = false;
bool keys_[512] = {};
bool prevKeys_[512] = {};
glm::vec2 lastMouse_{0.0f};
glm::vec2 mouseDelta_{0.0f};
float scrollAccum_ = 0.0f;
float scrollDelta_ = 0.0f;
};
} // namespace hd2d