init
This commit is contained in:
49
engine/include/hd2d/Billboard.h
Normal file
49
engine/include/hd2d/Billboard.h
Normal 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
|
||||
24
engine/include/hd2d/Camera.h
Normal file
24
engine/include/hd2d/Camera.h
Normal 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
|
||||
29
engine/include/hd2d/DebugUI.h
Normal file
29
engine/include/hd2d/DebugUI.h
Normal 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
4
engine/include/hd2d/GL.h
Normal 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>
|
||||
35
engine/include/hd2d/GroundOcclusion.h
Normal file
35
engine/include/hd2d/GroundOcclusion.h
Normal 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
|
||||
5
engine/include/hd2d/Log.h
Normal file
5
engine/include/hd2d/Log.h
Normal 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
106
engine/include/hd2d/Model.h
Normal 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
|
||||
85
engine/include/hd2d/Particles.h
Normal file
85
engine/include/hd2d/Particles.h
Normal 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
|
||||
40
engine/include/hd2d/RenderTarget.h
Normal file
40
engine/include/hd2d/RenderTarget.h
Normal 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
|
||||
100
engine/include/hd2d/Renderer.h
Normal file
100
engine/include/hd2d/Renderer.h
Normal 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
|
||||
60
engine/include/hd2d/Scene.h
Normal file
60
engine/include/hd2d/Scene.h
Normal 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
|
||||
9
engine/include/hd2d/Screenshot.h
Normal file
9
engine/include/hd2d/Screenshot.h
Normal 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
|
||||
42
engine/include/hd2d/Shader.h
Normal file
42
engine/include/hd2d/Shader.h
Normal 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
|
||||
61
engine/include/hd2d/Terrain.h
Normal file
61
engine/include/hd2d/Terrain.h
Normal 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
|
||||
68
engine/include/hd2d/Texture.h
Normal file
68
engine/include/hd2d/Texture.h
Normal 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
|
||||
50
engine/include/hd2d/Water.h
Normal file
50
engine/include/hd2d/Water.h
Normal 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
|
||||
54
engine/include/hd2d/Weather.h
Normal file
54
engine/include/hd2d/Weather.h
Normal 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
|
||||
49
engine/include/hd2d/Window.h
Normal file
49
engine/include/hd2d/Window.h
Normal 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
|
||||
Reference in New Issue
Block a user