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
|
||||
42
engine/src/Billboard.cpp
Normal file
42
engine/src/Billboard.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#include "hd2d/Billboard.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
static_assert(sizeof(BillboardInstance) == 32, "instance layout must match billboard.vert");
|
||||
|
||||
BillboardBatch::~BillboardBatch() {
|
||||
if (vbo_) glDeleteBuffers(1, &vbo_);
|
||||
if (vao_) glDeleteVertexArrays(1, &vao_);
|
||||
}
|
||||
|
||||
void BillboardBatch::upload() {
|
||||
if (!vao_) {
|
||||
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);
|
||||
}
|
||||
count_ = GLsizei(instances.size());
|
||||
if (instances.size() > capacity_) {
|
||||
capacity_ = instances.size();
|
||||
glNamedBufferData(vbo_, GLsizeiptr(capacity_ * sizeof(BillboardInstance)), nullptr, GL_DYNAMIC_DRAW);
|
||||
glVertexArrayVertexBuffer(vao_, 0, vbo_, 0, sizeof(BillboardInstance));
|
||||
}
|
||||
if (count_)
|
||||
glNamedBufferSubData(vbo_, 0, GLsizeiptr(count_ * sizeof(BillboardInstance)), instances.data());
|
||||
}
|
||||
|
||||
void BillboardBatch::draw() const {
|
||||
if (!count_) return;
|
||||
glBindVertexArray(vao_);
|
||||
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, count_);
|
||||
}
|
||||
|
||||
} // namespace hd2d
|
||||
19
engine/src/Camera.cpp
Normal file
19
engine/src/Camera.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "hd2d/Camera.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
glm::vec3 Camera::forward() const {
|
||||
float cp = std::cos(pitch);
|
||||
return glm::normalize(glm::vec3(std::sin(yaw) * cp, -std::sin(pitch), -std::cos(yaw) * cp));
|
||||
}
|
||||
|
||||
glm::vec3 Camera::right() const { return glm::vec3(std::cos(yaw), 0.0f, std::sin(yaw)); }
|
||||
|
||||
glm::mat4 Camera::view() const { return glm::lookAt(position(), target, glm::vec3(0, 1, 0)); }
|
||||
|
||||
glm::mat4 Camera::projection(float aspect) const { return glm::perspective(fovY, aspect, nearPlane, farPlane); }
|
||||
|
||||
} // namespace hd2d
|
||||
164
engine/src/DebugUI.cpp
Normal file
164
engine/src/DebugUI.cpp
Normal file
@@ -0,0 +1,164 @@
|
||||
#include "hd2d/DebugUI.h"
|
||||
|
||||
#include "hd2d/Renderer.h"
|
||||
#include "hd2d/Weather.h"
|
||||
#include "hd2d/Window.h"
|
||||
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <imgui.h>
|
||||
#include <imgui_impl_glfw.h>
|
||||
#include <imgui_impl_opengl3.h>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
bool DebugUI::init(Window& window) {
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImGui::GetIO().IniFilename = nullptr;
|
||||
ImGui::StyleColorsDark();
|
||||
ImGui::GetStyle().WindowRounding = 4.0f;
|
||||
if (!ImGui_ImplGlfw_InitForOpenGL(window.handle(), true)) return false;
|
||||
if (!ImGui_ImplOpenGL3_Init("#version 450")) return false;
|
||||
initialized_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DebugUI::shutdown() {
|
||||
if (!initialized_) return;
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplGlfw_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
initialized_ = false;
|
||||
}
|
||||
|
||||
void DebugUI::beginFrame() {
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplGlfw_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
}
|
||||
|
||||
void DebugUI::endFrame() {
|
||||
ImGui::Render();
|
||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||
}
|
||||
|
||||
bool DebugUI::wantsMouse() const { return initialized_ && ImGui::GetIO().WantCaptureMouse; }
|
||||
bool DebugUI::wantsKeyboard() const { return initialized_ && ImGui::GetIO().WantCaptureKeyboard; }
|
||||
|
||||
void DebugUI::renderSettingsPanel(RenderSettings& s, Scene& scene, float gpuMs) {
|
||||
ImGui::SetNextWindowPos(ImVec2(12, 12), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(340, 640), ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin("HD-2D Renderer");
|
||||
ImGui::Text("%.1f fps | GPU %.2f ms", ImGui::GetIO().Framerate, gpuMs);
|
||||
ImGui::Checkbox("Fade scenery hiding the player", &s.occluderFade);
|
||||
|
||||
if (ImGui::CollapsingHeader("Sun & Sky", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::SliderAngle("Azimuth", &scene.sun.azimuth, -180.0f, 180.0f);
|
||||
ImGui::SliderAngle("Elevation", &scene.sun.elevation, 3.0f, 89.0f);
|
||||
ImGui::ColorEdit3("Sun color", glm::value_ptr(scene.sun.color));
|
||||
ImGui::SliderFloat("Sun intensity", &scene.sun.intensity, 0.0f, 10.0f);
|
||||
ImGui::ColorEdit3("Zenith", glm::value_ptr(scene.sky.zenith));
|
||||
ImGui::ColorEdit3("Horizon", glm::value_ptr(scene.sky.horizon));
|
||||
ImGui::ColorEdit3("Ground bounce", glm::value_ptr(scene.sky.ground));
|
||||
ImGui::SliderFloat("Ambient", &scene.sky.ambient, 0.0f, 2.0f);
|
||||
ImGui::SliderFloat("Camera fill", &scene.sky.fill, 0.0f, 1.5f);
|
||||
ImGui::SliderFloat("Shadow softness", &s.shadowSoftness, 0.0f, 4.0f);
|
||||
}
|
||||
if (ImGui::CollapsingHeader("Canopy light (cookie)")) {
|
||||
ImGui::SliderFloat("Strength##ck", &s.cookieStrength, 0.0f, 1.0f);
|
||||
ImGui::SliderFloat("Scale##ck", &s.cookieScale, 0.02f, 0.5f);
|
||||
ImGui::SliderFloat("Coverage##ck", &s.cookieCoverage, 0.2f, 0.8f);
|
||||
ImGui::SliderFloat("Drift speed##ck", &s.cookieSpeed, 0.0f, 0.3f);
|
||||
}
|
||||
if (ImGui::CollapsingHeader("Ambient occlusion")) {
|
||||
ImGui::Checkbox("SSAO", &s.ssao);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Ground contact AO", &s.groundOcclusion);
|
||||
ImGui::SliderFloat("Radius##ao", &s.ssaoRadius, 0.1f, 3.0f);
|
||||
ImGui::SliderFloat("Intensity##ao", &s.ssaoIntensity, 0.0f, 3.0f);
|
||||
ImGui::TextDisabled("Ground contact AO is baked per ModelBatch");
|
||||
}
|
||||
if (ImGui::CollapsingHeader("Fog & God rays", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::Checkbox("Volumetrics", &s.volumetrics);
|
||||
ImGui::ColorEdit3("Fog color", glm::value_ptr(s.fogColor));
|
||||
ImGui::SliderFloat("Density", &s.fogDensity, 0.0f, 0.2f, "%.4f");
|
||||
ImGui::SliderFloat("Height falloff", &s.fogHeightFalloff, 0.0f, 1.5f);
|
||||
ImGui::SliderFloat("Base height", &s.fogBaseHeight, -5.0f, 10.0f);
|
||||
ImGui::SliderFloat("Noise", &s.fogNoise, 0.0f, 1.0f);
|
||||
ImGui::SliderFloat("Anisotropy", &s.fogAnisotropy, 0.0f, 0.95f);
|
||||
ImGui::SliderFloat("Shaft strength", &s.shaftStrength, 0.0f, 1.0f);
|
||||
ImGui::SliderFloat("Shaft coverage", &s.shaftCoverage, 0.2f, 0.8f);
|
||||
ImGui::SliderFloat("Shaft scale", &s.shaftScale, 0.05f, 1.5f);
|
||||
ImGui::SliderFloat("God ray intensity", &s.godRayIntensity, 0.0f, 6.0f);
|
||||
ImGui::SliderInt("Steps", &s.fogSteps, 8, 128);
|
||||
}
|
||||
if (scene.water && ImGui::CollapsingHeader("Water")) {
|
||||
Water& w = *scene.water;
|
||||
ImGui::Checkbox("Reflections", &s.reflections);
|
||||
ImGui::ColorEdit3("Shallow", glm::value_ptr(w.shallowColor));
|
||||
ImGui::ColorEdit3("Deep", glm::value_ptr(w.deepColor));
|
||||
ImGui::SliderFloat3("Absorption", glm::value_ptr(w.absorption), 0.0f, 2.0f);
|
||||
ImGui::SliderFloat("Clarity", &w.clarity, 0.05f, 2.0f);
|
||||
ImGui::SliderFloat("Wave scale", &w.waveScale, 0.1f, 4.0f);
|
||||
ImGui::SliderFloat("Wave strength", &w.waveStrength, 0.0f, 2.0f);
|
||||
ImGui::SliderFloat("Wave speed", &w.waveSpeed, 0.0f, 2.0f);
|
||||
ImGui::SliderFloat("Refraction", &w.refraction, 0.0f, 0.1f);
|
||||
ImGui::SliderFloat("Reflection distort", &w.reflectionDistort, 0.0f, 0.1f);
|
||||
ImGui::SliderFloat("Glitter", &w.glitter, 0.0f, 4.0f);
|
||||
ImGui::SliderFloat("Specular", &w.specular, 0.0f, 20.0f);
|
||||
ImGui::SliderFloat("Foam width", &w.foamWidth, 0.0f, 1.0f);
|
||||
ImGui::SliderFloat("Foam intensity", &w.foamIntensity, 0.0f, 1.0f);
|
||||
ImGui::SliderFloat("Caustics", &w.causticsIntensity, 0.0f, 4.0f);
|
||||
}
|
||||
if (ImGui::CollapsingHeader("Wind")) {
|
||||
ImGui::SliderFloat2("Direction", glm::value_ptr(s.windDirection), -1.0f, 1.0f);
|
||||
ImGui::SliderFloat("Strength##wind", &s.windStrength, 0.0f, 0.3f);
|
||||
ImGui::SliderFloat("Gust speed", &s.windGustSpeed, 0.0f, 4.0f);
|
||||
}
|
||||
if (ImGui::CollapsingHeader("Depth of field", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::Checkbox("Enabled##dof", &s.dof);
|
||||
ImGui::SliderFloat("Focus range", &s.focusRange, 0.0f, 20.0f);
|
||||
ImGui::SliderFloat("Transition", &s.focusTransition, 0.1f, 30.0f);
|
||||
ImGui::SliderFloat("Max radius", &s.dofMaxRadius, 1.0f, 20.0f);
|
||||
ImGui::SliderFloat("Tilt-shift", &s.tiltShift, 0.0f, 1.0f);
|
||||
}
|
||||
if (ImGui::CollapsingHeader("Bloom & Grade")) {
|
||||
ImGui::Checkbox("Bloom", &s.bloom);
|
||||
ImGui::SliderFloat("Bloom strength", &s.bloomStrength, 0.0f, 0.4f);
|
||||
ImGui::SliderFloat("Bloom radius", &s.bloomRadius, 0.5f, 3.0f);
|
||||
ImGui::SliderFloat("Exposure", &s.exposure, 0.1f, 4.0f);
|
||||
ImGui::SliderFloat("Contrast", &s.contrast, 0.5f, 1.6f);
|
||||
ImGui::SliderFloat("Saturation", &s.saturation, 0.0f, 2.0f);
|
||||
ImGui::ColorEdit3("Lift", glm::value_ptr(s.lift));
|
||||
ImGui::ColorEdit3("Gain", glm::value_ptr(s.gain));
|
||||
ImGui::SliderFloat("Vignette", &s.vignette, 0.0f, 1.0f);
|
||||
ImGui::SliderFloat("Chromatic ab.", &s.chromaticAberration, 0.0f, 0.01f, "%.4f");
|
||||
ImGui::SliderFloat("Grain", &s.grain, 0.0f, 0.1f);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void DebugUI::weatherPanel(Weather& w) {
|
||||
ImGui::SetNextWindowPos(ImVec2(364, 12), ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin("Weather", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
|
||||
float target = w.targetRain();
|
||||
if (ImGui::SliderFloat("Rain", &target, 0.0f, 1.0f)) w.setRain(target);
|
||||
ImGui::Text("current %.2f (R toggles)", w.rain());
|
||||
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::TreePop();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
} // namespace hd2d
|
||||
57
engine/src/GroundOcclusion.cpp
Normal file
57
engine/src/GroundOcclusion.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include "hd2d/GroundOcclusion.h"
|
||||
|
||||
#include "hd2d/Model.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
GroundOcclusion::~GroundOcclusion() {
|
||||
if (tex_) glDeleteTextures(1, &tex_);
|
||||
}
|
||||
|
||||
void GroundOcclusion::build(glm::vec2 min, glm::vec2 max, const std::vector<ModelBatch*>& batches,
|
||||
int texelsPerUnit) {
|
||||
const int w = std::max(1, int(std::ceil((max.x - min.x) * float(texelsPerUnit))));
|
||||
const int h = std::max(1, int(std::ceil((max.y - min.y) * float(texelsPerUnit))));
|
||||
std::vector<float> occ(size_t(w) * h, 1.0f);
|
||||
const float tpu = float(texelsPerUnit);
|
||||
|
||||
for (const ModelBatch* b : batches) {
|
||||
if (!b || !b->model || b->groundOcclusion <= 0.0f) continue;
|
||||
const glm::vec3 ext = b->model->boundsMax - b->model->boundsMin;
|
||||
for (const ModelInstance& in : b->instances) {
|
||||
float footprint = 0.5f * std::max(ext.x * in.scale.x, ext.z * in.scale.z);
|
||||
float height = b->model->boundsMax.y * in.scale.y;
|
||||
bool tall = height > 1.6f * footprint; // trees: trunk contact + canopy shade
|
||||
float rCore = tall ? 0.15f * footprint + 0.45f : 0.8f * footprint;
|
||||
float rWide = tall ? 0.85f * footprint : 1.3f * footprint;
|
||||
float kCore = 0.85f * b->groundOcclusion, kWide = (tall ? 0.4f : 0.3f) * b->groundOcclusion;
|
||||
float reach = std::max(rCore, rWide) * 2.2f;
|
||||
int x0 = std::max(0, int((in.pos.x - reach - min.x) * tpu));
|
||||
int x1 = std::min(w - 1, int((in.pos.x + reach - min.x) * tpu));
|
||||
int z0 = std::max(0, int((in.pos.z - reach - min.y) * tpu));
|
||||
int z1 = std::min(h - 1, int((in.pos.z + reach - min.y) * tpu));
|
||||
for (int z = z0; z <= z1; ++z)
|
||||
for (int x = x0; x <= x1; ++x) {
|
||||
glm::vec2 p(min.x + (float(x) + 0.5f) / tpu, min.y + (float(z) + 0.5f) / tpu);
|
||||
float d = glm::length(p - glm::vec2(in.pos.x, in.pos.z));
|
||||
float a = kCore * std::exp(-(d * d) / (rCore * rCore)) + kWide * std::exp(-(d * d) / (rWide * rWide));
|
||||
occ[size_t(z) * w + x] *= 1.0f - std::min(a, 0.9f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tex_) glDeleteTextures(1, &tex_);
|
||||
glCreateTextures(GL_TEXTURE_2D, 1, &tex_);
|
||||
glTextureStorage2D(tex_, 1, GL_R16F, w, h);
|
||||
glTextureSubImage2D(tex_, 0, 0, 0, w, h, GL_RED, GL_FLOAT, occ.data());
|
||||
glTextureParameteri(tex_, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTextureParameteri(tex_, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTextureParameteri(tex_, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTextureParameteri(tex_, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
rect_ = glm::vec4(min.x, min.y, 1.0f / (max.x - min.x), 1.0f / (max.y - min.y));
|
||||
}
|
||||
|
||||
} // namespace hd2d
|
||||
221
engine/src/Model.cpp
Normal file
221
engine/src/Model.cpp
Normal file
@@ -0,0 +1,221 @@
|
||||
#include "hd2d/Model.h"
|
||||
|
||||
#include "hd2d/Log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
|
||||
#define CGLTF_IMPLEMENTATION
|
||||
#include <cgltf.h>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Vertex {
|
||||
glm::vec3 pos;
|
||||
glm::vec3 normal;
|
||||
glm::vec2 uv;
|
||||
glm::vec4 color;
|
||||
};
|
||||
|
||||
void readAttr(const cgltf_accessor* acc, size_t comps, std::vector<float>& out) {
|
||||
out.assign(acc->count * comps, 0.0f);
|
||||
for (size_t i = 0; i < acc->count; ++i) cgltf_accessor_read_float(acc, i, &out[i * comps], comps);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ModelLibrary::~ModelLibrary() {
|
||||
for (auto& [name, m] : models_)
|
||||
for (ModelPrimitive& p : m->primitives) {
|
||||
glDeleteBuffers(1, &p.vbo);
|
||||
glDeleteBuffers(1, &p.ibo);
|
||||
}
|
||||
}
|
||||
|
||||
bool ModelLibrary::load(const std::string& path) {
|
||||
cgltf_options options{};
|
||||
cgltf_data* data = nullptr;
|
||||
if (cgltf_parse_file(&options, path.c_str(), &data) != cgltf_result_success ||
|
||||
cgltf_load_buffers(&options, data, path.c_str()) != cgltf_result_success) {
|
||||
HD2D_ERR("cannot load model file '%s'", path.c_str());
|
||||
if (data) cgltf_free(data);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Images -> textures (decoded once, shared by every material).
|
||||
std::vector<const Texture*> imageTex(data->images_count, nullptr);
|
||||
for (size_t i = 0; i < data->images_count; ++i) {
|
||||
const cgltf_image& im = data->images[i];
|
||||
if (!im.buffer_view) continue; // external URIs are not used by our pipeline
|
||||
const auto* bytes = static_cast<const unsigned char*>(im.buffer_view->buffer->data) + im.buffer_view->offset;
|
||||
Image img;
|
||||
if (!decodeImage(bytes, im.buffer_view->size, img, false)) continue;
|
||||
auto tex = std::make_unique<Texture>();
|
||||
tex->create(img, true, 8.0f);
|
||||
imageTex[i] = tex.get();
|
||||
textures_.push_back(std::move(tex));
|
||||
}
|
||||
|
||||
size_t matBase = materials_.size(); // this file's materials start here
|
||||
for (size_t i = 0; i < data->materials_count; ++i) {
|
||||
const cgltf_material& m = data->materials[i];
|
||||
ModelMaterial mm;
|
||||
mm.name = m.name ? m.name : "";
|
||||
mm.foliage = mm.name.rfind("foliage", 0) == 0;
|
||||
mm.doubleSided = m.double_sided || mm.foliage;
|
||||
mm.alphaMask = m.alpha_mode == cgltf_alpha_mode_mask;
|
||||
mm.alphaCutoff = m.alpha_cutoff > 0.0f ? m.alpha_cutoff : 0.5f;
|
||||
if (m.has_pbr_metallic_roughness) {
|
||||
const auto& pbr = m.pbr_metallic_roughness;
|
||||
mm.baseColor = glm::make_vec4(pbr.base_color_factor);
|
||||
if (pbr.base_color_texture.texture && pbr.base_color_texture.texture->image)
|
||||
mm.texture = imageTex[size_t(pbr.base_color_texture.texture->image - data->images)];
|
||||
}
|
||||
float strength = m.has_emissive_strength ? m.emissive_strength.emissive_strength : 1.0f;
|
||||
mm.emissive = glm::make_vec3(m.emissive_factor) * strength;
|
||||
if (m.emissive_texture.texture && m.emissive_texture.texture->image)
|
||||
mm.emissiveTexture = imageTex[size_t(m.emissive_texture.texture->image - data->images)];
|
||||
materials_.push_back(std::make_unique<ModelMaterial>(mm));
|
||||
}
|
||||
if (materials_.size() == matBase) materials_.push_back(std::make_unique<ModelMaterial>()); // default
|
||||
|
||||
for (size_t n = 0; n < data->nodes_count; ++n) {
|
||||
const cgltf_node& node = data->nodes[n];
|
||||
if (node.parent || !node.mesh || !node.name) continue;
|
||||
float world[16];
|
||||
cgltf_node_transform_world(&node, world);
|
||||
glm::mat4 M = glm::make_mat4(world);
|
||||
glm::mat3 N = glm::transpose(glm::inverse(glm::mat3(M)));
|
||||
|
||||
auto model = std::make_unique<Model>();
|
||||
model->name = node.name;
|
||||
model->boundsMin = glm::vec3(1e30f);
|
||||
model->boundsMax = glm::vec3(-1e30f);
|
||||
for (size_t c = 0; c < node.children_count; ++c) {
|
||||
const cgltf_node* child = node.children[c];
|
||||
if (child->mesh || !child->name) continue;
|
||||
float cw[16];
|
||||
cgltf_node_transform_world(child, cw);
|
||||
model->sockets[child->name] = glm::vec3(cw[12], cw[13], cw[14]);
|
||||
}
|
||||
for (size_t pi = 0; pi < node.mesh->primitives_count; ++pi) {
|
||||
const cgltf_primitive& prim = node.mesh->primitives[pi];
|
||||
if (prim.type != cgltf_primitive_type_triangles) continue;
|
||||
std::vector<float> pos, nrm, uv, col;
|
||||
for (size_t a = 0; a < prim.attributes_count; ++a) {
|
||||
const cgltf_attribute& at = prim.attributes[a];
|
||||
if (at.type == cgltf_attribute_type_position) readAttr(at.data, 3, pos);
|
||||
else if (at.type == cgltf_attribute_type_normal) readAttr(at.data, 3, nrm);
|
||||
else if (at.type == cgltf_attribute_type_texcoord && at.index == 0) readAttr(at.data, 2, uv);
|
||||
else if (at.type == cgltf_attribute_type_color && at.index == 0)
|
||||
readAttr(at.data, cgltf_num_components(at.data->type), col);
|
||||
}
|
||||
size_t count = pos.size() / 3;
|
||||
if (!count) continue;
|
||||
size_t colComps = count ? col.size() / count : 0;
|
||||
std::vector<Vertex> verts(count);
|
||||
for (size_t v = 0; v < count; ++v) {
|
||||
Vertex& o = verts[v];
|
||||
o.pos = glm::vec3(M * glm::vec4(pos[v * 3], pos[v * 3 + 1], pos[v * 3 + 2], 1.0f));
|
||||
o.normal = nrm.empty() ? glm::vec3(0, 1, 0)
|
||||
: glm::normalize(N * glm::vec3(nrm[v * 3], nrm[v * 3 + 1], nrm[v * 3 + 2]));
|
||||
o.uv = uv.empty() ? glm::vec2(0.0f) : glm::vec2(uv[v * 2], uv[v * 2 + 1]);
|
||||
o.color = glm::vec4(1.0f, 1.0f, 1.0f, 0.0f); // no color: untinted and rigid
|
||||
if (colComps >= 3) o.color = glm::vec4(col[v * colComps], col[v * colComps + 1], col[v * colComps + 2],
|
||||
colComps == 4 ? col[v * colComps + 3] : 0.0f);
|
||||
model->boundsMin = glm::min(model->boundsMin, o.pos);
|
||||
model->boundsMax = glm::max(model->boundsMax, o.pos);
|
||||
}
|
||||
std::vector<uint32_t> idx;
|
||||
if (prim.indices) {
|
||||
idx.resize(prim.indices->count);
|
||||
for (size_t i = 0; i < idx.size(); ++i) idx[i] = uint32_t(cgltf_accessor_read_index(prim.indices, i));
|
||||
} else {
|
||||
idx.resize(count);
|
||||
for (size_t i = 0; i < count; ++i) idx[i] = uint32_t(i);
|
||||
}
|
||||
|
||||
ModelPrimitive mp;
|
||||
mp.material = materials_[prim.material ? matBase + size_t(prim.material - data->materials) : matBase].get();
|
||||
mp.indexCount = GLsizei(idx.size());
|
||||
glCreateBuffers(1, &mp.vbo);
|
||||
glNamedBufferStorage(mp.vbo, GLsizeiptr(verts.size() * sizeof(Vertex)), verts.data(), 0);
|
||||
glCreateBuffers(1, &mp.ibo);
|
||||
glNamedBufferStorage(mp.ibo, GLsizeiptr(idx.size() * sizeof(uint32_t)), idx.data(), 0);
|
||||
model->primitives.push_back(mp);
|
||||
}
|
||||
// Opaque first, then alpha-tested: fewer overdraw costs from early depth rejection.
|
||||
std::stable_sort(model->primitives.begin(), model->primitives.end(), [&](const auto& a, const auto& b) {
|
||||
return !a.material->alphaMask && b.material->alphaMask;
|
||||
});
|
||||
models_[model->name] = std::move(model);
|
||||
}
|
||||
HD2D_LOG("loaded %zu models, %zu textures from %s", models_.size(), textures_.size(), path.c_str());
|
||||
cgltf_free(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
const Model* ModelLibrary::get(const std::string& name) const {
|
||||
auto it = models_.find(name);
|
||||
if (it == models_.end()) {
|
||||
HD2D_ERR("model '%s' not found", name.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
static_assert(sizeof(ModelInstance) == 32, "instance layout must match mesh.vert");
|
||||
|
||||
ModelBatch::~ModelBatch() {
|
||||
if (!vaos_.empty()) glDeleteVertexArrays(GLsizei(vaos_.size()), vaos_.data());
|
||||
if (instanceVbo_) glDeleteBuffers(1, &instanceVbo_);
|
||||
}
|
||||
|
||||
void ModelBatch::upload() {
|
||||
if (!model) return;
|
||||
if (!instanceVbo_) glCreateBuffers(1, &instanceVbo_);
|
||||
count_ = GLsizei(instances.size());
|
||||
if (instances.size() > capacity_) {
|
||||
capacity_ = instances.size();
|
||||
glNamedBufferData(instanceVbo_, GLsizeiptr(capacity_ * sizeof(ModelInstance)), nullptr, GL_DYNAMIC_DRAW);
|
||||
}
|
||||
if (count_)
|
||||
glNamedBufferSubData(instanceVbo_, 0, GLsizeiptr(count_ * sizeof(ModelInstance)), instances.data());
|
||||
if (vaos_.empty()) {
|
||||
vaos_.resize(model->primitives.size());
|
||||
glCreateVertexArrays(GLsizei(vaos_.size()), vaos_.data());
|
||||
for (size_t i = 0; i < vaos_.size(); ++i) {
|
||||
GLuint vao = vaos_[i];
|
||||
const ModelPrimitive& p = model->primitives[i];
|
||||
glVertexArrayVertexBuffer(vao, 0, p.vbo, 0, sizeof(Vertex));
|
||||
glVertexArrayElementBuffer(vao, p.ibo);
|
||||
const GLuint offsets[4] = {offsetof(Vertex, pos), offsetof(Vertex, normal), offsetof(Vertex, uv),
|
||||
offsetof(Vertex, color)};
|
||||
const GLint sizes[4] = {3, 3, 2, 4};
|
||||
for (GLuint a = 0; a < 4; ++a) {
|
||||
glEnableVertexArrayAttrib(vao, a);
|
||||
glVertexArrayAttribFormat(vao, a, sizes[a], GL_FLOAT, GL_FALSE, offsets[a]);
|
||||
glVertexArrayAttribBinding(vao, a, 0);
|
||||
}
|
||||
glVertexArrayBindingDivisor(vao, 1, 1);
|
||||
for (GLuint a = 4; a < 6; ++a) {
|
||||
glEnableVertexArrayAttrib(vao, a);
|
||||
glVertexArrayAttribFormat(vao, a, 4, GL_FLOAT, GL_FALSE, (a - 4) * 16);
|
||||
glVertexArrayAttribBinding(vao, a, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (GLuint vao : vaos_) glVertexArrayVertexBuffer(vao, 1, instanceVbo_, 0, sizeof(ModelInstance));
|
||||
}
|
||||
|
||||
void ModelBatch::drawPrimitive(size_t i) const {
|
||||
if (!count_) return;
|
||||
glBindVertexArray(vaos_[i]);
|
||||
glDrawElementsInstanced(GL_TRIANGLES, model->primitives[i].indexCount, GL_UNSIGNED_INT, nullptr, count_);
|
||||
}
|
||||
|
||||
} // namespace hd2d
|
||||
262
engine/src/Particles.cpp
Normal file
262
engine/src/Particles.cpp
Normal file
@@ -0,0 +1,262 @@
|
||||
#include "hd2d/Particles.h"
|
||||
|
||||
#include "hd2d/Scene.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
namespace {
|
||||
|
||||
glm::vec3 flameColor(float t) {
|
||||
// Yellow core -> orange -> deep red, in linear HDR so the core blooms like
|
||||
// HD-2D's torches. Kept saturated: overlapping tongues add up quickly.
|
||||
const glm::vec3 c0(4.0f, 2.3f, 0.6f), c1(3.6f, 1.0f, 0.15f), c2(1.8f, 0.26f, 0.04f), c3(0.3f, 0.03f, 0.01f);
|
||||
if (t < 0.25f) return glm::mix(c0, c1, t / 0.25f);
|
||||
if (t < 0.6f) return glm::mix(c1, c2, (t - 0.25f) / 0.35f);
|
||||
return glm::mix(c2, c3, (t - 0.6f) / 0.4f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ParticleSystem::~ParticleSystem() {
|
||||
if (vbo_) glDeleteBuffers(1, &vbo_);
|
||||
if (vao_) glDeleteVertexArrays(1, &vao_);
|
||||
}
|
||||
|
||||
void ParticleSystem::addField(Kind kind, glm::vec3 min, glm::vec3 max, int count) {
|
||||
fields_.push_back({kind, min, max, count});
|
||||
int fi = int(fields_.size()) - 1;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Particle p{};
|
||||
spawnInField(p, fi, true);
|
||||
particles_.push_back(p);
|
||||
}
|
||||
}
|
||||
|
||||
int ParticleSystem::addFire(glm::vec3 pos, float scale) {
|
||||
std::uniform_real_distribution<float> u(0.0f, 100.0f);
|
||||
Fire f;
|
||||
f.pos = pos;
|
||||
f.scale = scale;
|
||||
f.phase = u(rng_);
|
||||
fires_.push_back(f);
|
||||
return int(fires_.size()) - 1;
|
||||
}
|
||||
|
||||
void ParticleSystem::spawnInField(Particle& p, int fi, bool randomAge) {
|
||||
std::uniform_real_distribution<float> u(0.0f, 1.0f);
|
||||
const Field& f = fields_[fi];
|
||||
p.kind = f.kind;
|
||||
p.field = fi;
|
||||
p.fire = -1;
|
||||
p.pos = f.min + (f.max - f.min) * glm::vec3(u(rng_), u(rng_), u(rng_));
|
||||
p.vel = glm::vec3(0.0f);
|
||||
p.phase = u(rng_) * 100.0f;
|
||||
if (f.kind == Kind::Firefly) {
|
||||
p.life = 7.0f + 6.0f * u(rng_);
|
||||
p.size = 0.07f + 0.04f * u(rng_);
|
||||
p.color = fireflyColor;
|
||||
} else {
|
||||
p.life = 9.0f + 8.0f * u(rng_);
|
||||
p.size = 0.025f + 0.025f * u(rng_);
|
||||
p.color = moteColor;
|
||||
}
|
||||
p.age = randomAge ? u(rng_) * p.life : 0.0f;
|
||||
}
|
||||
|
||||
void ParticleSystem::spawnFire(const Fire& f, int fireIndex, Kind kind) {
|
||||
std::uniform_real_distribution<float> u(0.0f, 1.0f);
|
||||
std::uniform_real_distribution<float> s(-1.0f, 1.0f);
|
||||
const float k = f.scale;
|
||||
Particle p{};
|
||||
p.kind = kind;
|
||||
p.field = -1;
|
||||
p.fire = fireIndex;
|
||||
p.phase = u(rng_) * 100.0f;
|
||||
p.age = 0.0f;
|
||||
float a = u(rng_) * 6.2832f;
|
||||
if (kind == Kind::Flame) {
|
||||
float r = 0.2f * k * std::sqrt(u(rng_));
|
||||
p.pos = f.pos + glm::vec3(std::cos(a) * r, u(rng_) * 0.05f * k, std::sin(a) * r);
|
||||
p.vel = glm::vec3(s(rng_) * 0.15f, 1.3f + 0.8f * u(rng_), s(rng_) * 0.15f) * std::sqrt(k);
|
||||
p.life = 0.5f + 0.4f * u(rng_);
|
||||
p.size = 0.42f * k * (0.8f + 0.4f * u(rng_));
|
||||
} else if (kind == Kind::Ember) {
|
||||
float r = 0.15f * k * u(rng_);
|
||||
p.pos = f.pos + glm::vec3(std::cos(a) * r, 0.1f * k, std::sin(a) * r);
|
||||
p.vel = glm::vec3(s(rng_) * 0.5f, 1.2f + 1.2f * u(rng_), s(rng_) * 0.5f);
|
||||
p.life = 1.0f + 1.2f * u(rng_);
|
||||
p.size = 0.022f + 0.014f * u(rng_);
|
||||
} else { // Smoke
|
||||
p.pos = f.pos + glm::vec3(s(rng_) * 0.08f, 0.55f * k, s(rng_) * 0.08f);
|
||||
p.vel = glm::vec3(s(rng_) * 0.1f, 0.5f + 0.3f * u(rng_), s(rng_) * 0.1f);
|
||||
p.life = 2.5f + 1.5f * u(rng_);
|
||||
p.size = 0.25f * k;
|
||||
}
|
||||
particles_.push_back(p);
|
||||
}
|
||||
|
||||
void ParticleSystem::burst(glm::vec3 pos, int count, glm::vec3 color, float speed, float size, float life) {
|
||||
std::uniform_real_distribution<float> u(-1.0f, 1.0f);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Particle p{};
|
||||
p.kind = Kind::Spark;
|
||||
p.field = -1;
|
||||
p.fire = -1;
|
||||
p.pos = pos;
|
||||
p.vel = glm::vec3(u(rng_), 0.6f + 0.4f * std::abs(u(rng_)), u(rng_)) * speed;
|
||||
p.color = color;
|
||||
p.size = size * (0.7f + 0.3f * u(rng_));
|
||||
p.life = life * (0.7f + 0.3f * std::abs(u(rng_)));
|
||||
p.age = 0.0f;
|
||||
p.phase = 0.0f;
|
||||
particles_.push_back(p);
|
||||
}
|
||||
}
|
||||
|
||||
void ParticleSystem::update(float dt, float time) {
|
||||
gpu_.clear();
|
||||
|
||||
// Fire emitters: spawn, flicker, and one big glow sprite each.
|
||||
for (size_t i = 0; i < fires_.size(); ++i) {
|
||||
Fire& f = fires_[i];
|
||||
float t = time + f.phase;
|
||||
f.flicker = 0.82f + 0.1f * std::sin(t * 11.0f) + 0.08f * std::sin(t * 23.7f + 1.3f) + 0.06f * std::sin(t * 5.3f);
|
||||
auto emit = [&](float& acc, float rate, Kind kind) {
|
||||
acc += rate * dt;
|
||||
while (acc >= 1.0f) {
|
||||
acc -= 1.0f;
|
||||
spawnFire(f, int(i), kind);
|
||||
}
|
||||
};
|
||||
emit(f.flameAccum, 45.0f * f.scale, Kind::Flame);
|
||||
emit(f.emberAccum, 14.0f * f.scale, Kind::Ember);
|
||||
emit(f.smokeAccum, 6.0f * f.scale, Kind::Smoke);
|
||||
glm::vec3 glow = glm::vec3(1.0f, 0.45f, 0.13f) * 0.9f * f.flicker;
|
||||
gpu_.push_back({glm::vec4(f.pos + glm::vec3(0, 0.35f * f.scale, 0), 1.1f * f.scale * f.flicker),
|
||||
glm::vec4(glow, 1.0f), glm::vec4(float(int(Kind::Glow)), 0, 0, 0)});
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < particles_.size();) {
|
||||
Particle& p = particles_[i];
|
||||
p.age += dt;
|
||||
if (p.age >= p.life) {
|
||||
if (p.field >= 0) {
|
||||
spawnInField(p, p.field, false);
|
||||
} else {
|
||||
particles_[i] = particles_.back();
|
||||
particles_.pop_back();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
float t = time + p.phase;
|
||||
float lifeT = p.age / p.life;
|
||||
float fade = std::min(1.0f, std::min(p.age, p.life - p.age) / 1.5f);
|
||||
float alpha = fade;
|
||||
float size = p.size;
|
||||
glm::vec3 color = p.color;
|
||||
switch (p.kind) {
|
||||
case Kind::Firefly: {
|
||||
// Smooth wandering flight with a slow hovering bob.
|
||||
glm::vec3 wander(std::sin(t * 0.7f) + std::sin(t * 1.31f + 2.0f) * 0.5f,
|
||||
std::sin(t * 1.1f + 1.0f) * 0.6f,
|
||||
std::cos(t * 0.53f) + std::cos(t * 1.7f + 4.0f) * 0.5f);
|
||||
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;
|
||||
break;
|
||||
}
|
||||
case Kind::Mote: {
|
||||
glm::vec3 drift(std::sin(t * 0.21f) * 0.12f, std::sin(t * 0.37f) * 0.05f - 0.02f,
|
||||
std::cos(t * 0.17f) * 0.12f);
|
||||
p.pos += drift * dt;
|
||||
alpha *= ambientFade;
|
||||
break;
|
||||
}
|
||||
case Kind::Spark:
|
||||
p.vel.y -= 6.0f * dt;
|
||||
p.vel *= std::exp(-3.0f * dt);
|
||||
p.pos += p.vel * dt;
|
||||
alpha = 1.0f - lifeT;
|
||||
break;
|
||||
case Kind::Flame: {
|
||||
const Fire& f = fires_[p.fire];
|
||||
p.vel.y += 1.6f * dt; // buoyancy
|
||||
p.vel.x += (std::sin(t * 9.0f) * 0.9f - (p.pos.x - f.pos.x) * 5.0f) * dt; // flicker + taper
|
||||
p.vel.z += (std::cos(t * 7.3f) * 0.9f - (p.pos.z - f.pos.z) * 5.0f) * dt;
|
||||
p.pos += p.vel * dt;
|
||||
color = flameColor(lifeT) * 0.22f; // tongues overlap a lot; keep the sum orange, not white
|
||||
alpha = std::min(1.0f, lifeT / 0.08f) * std::pow(1.0f - lifeT, 1.3f);
|
||||
size = p.size * (0.75f + 0.5f * std::sin(3.1416f * std::min(lifeT * 1.2f, 1.0f))) * (1.0f - 0.5f * lifeT);
|
||||
break;
|
||||
}
|
||||
case Kind::Ember: {
|
||||
p.vel += (glm::vec3(std::sin(t * 3.0f + p.phase * 7.0f), 0.3f, std::cos(t * 2.6f + p.phase * 5.0f)) * 1.8f +
|
||||
wind * 0.6f) * dt;
|
||||
p.vel *= std::exp(-0.8f * dt);
|
||||
p.pos += p.vel * dt;
|
||||
float blink = 0.6f + 0.4f * std::sin(t * 17.0f);
|
||||
color = glm::vec3(6.0f, 2.2f, 0.5f) * blink * (1.0f - 0.6f * lifeT);
|
||||
alpha = 1.0f - lifeT;
|
||||
break;
|
||||
}
|
||||
case Kind::Smoke:
|
||||
p.vel += (wind * 0.6f + glm::vec3(0, 0.1f, 0)) * dt;
|
||||
p.pos += p.vel * dt;
|
||||
size = p.size * (1.0f + 2.5f * lifeT);
|
||||
color = glm::vec3(0.10f, 0.095f, 0.09f);
|
||||
alpha = 0.32f * std::min(1.0f, lifeT / 0.15f) * (1.0f - lifeT);
|
||||
break;
|
||||
case Kind::Glow:
|
||||
break;
|
||||
}
|
||||
if (p.field >= 0) { // keep ambient particles inside their field
|
||||
const Field& f = fields_[p.field];
|
||||
p.pos = glm::clamp(p.pos, f.min, f.max);
|
||||
}
|
||||
float seed = p.phase - std::floor(p.phase);
|
||||
gpu_.push_back({glm::vec4(p.pos, size), glm::vec4(color, alpha), glm::vec4(float(int(p.kind)), lifeT, seed, 0)});
|
||||
++i;
|
||||
}
|
||||
|
||||
if (!vao_) {
|
||||
glCreateVertexArrays(1, &vao_);
|
||||
glCreateBuffers(1, &vbo_);
|
||||
glVertexArrayBindingDivisor(vao_, 0, 1);
|
||||
for (GLuint a = 0; a < 3; ++a) {
|
||||
glEnableVertexArrayAttrib(vao_, a);
|
||||
glVertexArrayAttribFormat(vao_, a, 4, GL_FLOAT, GL_FALSE, GLuint(a * sizeof(glm::vec4)));
|
||||
glVertexArrayAttribBinding(vao_, a, 0);
|
||||
}
|
||||
}
|
||||
if (gpu_.size() > capacity_) {
|
||||
capacity_ = gpu_.size() * 2;
|
||||
glNamedBufferData(vbo_, GLsizeiptr(capacity_ * sizeof(GpuParticle)), nullptr, GL_STREAM_DRAW);
|
||||
glVertexArrayVertexBuffer(vao_, 0, vbo_, 0, sizeof(GpuParticle));
|
||||
}
|
||||
if (!gpu_.empty())
|
||||
glNamedBufferSubData(vbo_, 0, GLsizeiptr(gpu_.size() * sizeof(GpuParticle)), gpu_.data());
|
||||
}
|
||||
|
||||
void ParticleSystem::draw() const {
|
||||
if (gpu_.empty()) return;
|
||||
glBindVertexArray(vao_);
|
||||
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, GLsizei(gpu_.size()));
|
||||
}
|
||||
|
||||
void ParticleSystem::collectLights(std::vector<PointLight>& out, int maxFireflies) const {
|
||||
for (const Fire& f : fires_)
|
||||
out.push_back({f.pos + glm::vec3(0, 0.4f * f.scale, 0), fireLightRadius * f.scale, fireLightColor * f.flicker});
|
||||
int n = 0;
|
||||
for (const GpuParticle& g : gpu_) {
|
||||
if (n >= maxFireflies) break;
|
||||
if (int(g.params.x) != int(Kind::Firefly) || g.color.a < 0.02f) continue;
|
||||
out.push_back({glm::vec3(g.posSize), fireflyLightRadius, glm::vec3(g.color) * g.color.a * 0.35f});
|
||||
++n;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace hd2d
|
||||
61
engine/src/RenderTarget.cpp
Normal file
61
engine/src/RenderTarget.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
#include "hd2d/RenderTarget.h"
|
||||
|
||||
#include "hd2d/Log.h"
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
GLuint createTexture2D(GLenum internalFormat, int w, int h, GLenum filter, int levels) {
|
||||
GLuint t;
|
||||
glCreateTextures(GL_TEXTURE_2D, 1, &t);
|
||||
glTextureStorage2D(t, levels, internalFormat, w, h);
|
||||
GLenum minFilter = filter;
|
||||
if (levels > 1) minFilter = filter == GL_LINEAR ? GL_LINEAR_MIPMAP_NEAREST : GL_NEAREST_MIPMAP_NEAREST;
|
||||
glTextureParameteri(t, GL_TEXTURE_MIN_FILTER, minFilter);
|
||||
glTextureParameteri(t, GL_TEXTURE_MAG_FILTER, filter);
|
||||
glTextureParameteri(t, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTextureParameteri(t, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
return t;
|
||||
}
|
||||
|
||||
void RenderTarget::create(int w, int h, std::initializer_list<GLenum> colorFormats, GLenum depthFormat,
|
||||
GLuint sharedDepth, GLenum filter) {
|
||||
destroy();
|
||||
w_ = w;
|
||||
h_ = h;
|
||||
glCreateFramebuffers(1, &fbo_);
|
||||
std::vector<GLenum> drawBuffers;
|
||||
for (GLenum fmt : colorFormats) {
|
||||
GLuint t = createTexture2D(fmt, w, h, filter);
|
||||
glNamedFramebufferTexture(fbo_, GLenum(GL_COLOR_ATTACHMENT0 + colors_.size()), t, 0);
|
||||
drawBuffers.push_back(GLenum(GL_COLOR_ATTACHMENT0 + colors_.size()));
|
||||
colors_.push_back(t);
|
||||
}
|
||||
if (depthFormat) {
|
||||
depth_ = createTexture2D(depthFormat, w, h, GL_NEAREST);
|
||||
ownsDepth_ = true;
|
||||
} else if (sharedDepth) {
|
||||
depth_ = sharedDepth;
|
||||
ownsDepth_ = false;
|
||||
}
|
||||
if (depth_) glNamedFramebufferTexture(fbo_, GL_DEPTH_ATTACHMENT, depth_, 0);
|
||||
if (drawBuffers.empty()) glNamedFramebufferDrawBuffer(fbo_, GL_NONE);
|
||||
else glNamedFramebufferDrawBuffers(fbo_, GLsizei(drawBuffers.size()), drawBuffers.data());
|
||||
GLenum status = glCheckNamedFramebufferStatus(fbo_, GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) HD2D_ERR("framebuffer incomplete: 0x%x", status);
|
||||
}
|
||||
|
||||
void RenderTarget::destroy() {
|
||||
if (!colors_.empty()) glDeleteTextures(GLsizei(colors_.size()), colors_.data());
|
||||
colors_.clear();
|
||||
if (depth_ && ownsDepth_) glDeleteTextures(1, &depth_);
|
||||
depth_ = 0;
|
||||
if (fbo_) glDeleteFramebuffers(1, &fbo_);
|
||||
fbo_ = 0;
|
||||
}
|
||||
|
||||
void RenderTarget::bind() const {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo_);
|
||||
glViewport(0, 0, w_, h_);
|
||||
}
|
||||
|
||||
} // namespace hd2d
|
||||
584
engine/src/Renderer.cpp
Normal file
584
engine/src/Renderer.cpp
Normal file
@@ -0,0 +1,584 @@
|
||||
#include "hd2d/Renderer.h"
|
||||
|
||||
#include "hd2d/Log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
glm::vec3 Sun::direction() const {
|
||||
float ce = std::cos(elevation);
|
||||
return glm::normalize(glm::vec3(ce * std::sin(azimuth), std::sin(elevation), -ce * std::cos(azimuth)));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kMaxLights = 16;
|
||||
constexpr int kBloomMips = 6;
|
||||
|
||||
// Mirrors `Frame` in assets/shaders/common.glsl (std140: only mat4/vec4/ivec4).
|
||||
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;
|
||||
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");
|
||||
|
||||
} // namespace
|
||||
|
||||
struct Renderer::Impl {
|
||||
std::string dir;
|
||||
int w = 0, h = 0;
|
||||
const RenderSettings* settings = nullptr;
|
||||
|
||||
Shader terrain, terrainDepth, billboard, mesh, meshDepth, water, sky, volumetric, fogComposite, particles,
|
||||
dofPrep, dofBlur, dofComposite, bloomDown, bloomUp, tonemap, ssao, ssaoBlur, ssaoApply, rainStreaks;
|
||||
|
||||
RenderTarget shadow, reflection, hdr, post, sceneCopy, depthCopy, volume, dofHalf, dofBlurred, dofOut, ssaoRaw,
|
||||
ssaoBlurred;
|
||||
RenderTarget bloom[kBloomMips];
|
||||
GLuint postDepthFbo = 0; // post color + hdr depth, for depth-tested particles
|
||||
int shadowSize = 0;
|
||||
|
||||
GLuint ubo = 0, emptyVao = 0, whiteTex = 0;
|
||||
GLuint timers[2] = {};
|
||||
bool timerUsed[2] = {};
|
||||
int timerIdx = 0;
|
||||
FrameUBO frame{};
|
||||
|
||||
~Impl() {
|
||||
if (ubo) glDeleteBuffers(1, &ubo);
|
||||
if (emptyVao) glDeleteVertexArrays(1, &emptyVao);
|
||||
if (whiteTex) glDeleteTextures(1, &whiteTex);
|
||||
if (timers[0]) glDeleteQueries(2, timers);
|
||||
if (postDepthFbo) glDeleteFramebuffers(1, &postDepthFbo);
|
||||
}
|
||||
|
||||
std::string path(const char* name) const { return dir + "/" + name; }
|
||||
|
||||
bool loadShaders() {
|
||||
bool ok = true;
|
||||
auto ld = [&](Shader& s, const char* vs, const char* fs) { ok &= s.load(path(vs), path(fs)); };
|
||||
ld(terrain, "terrain.vert", "terrain.frag");
|
||||
ld(terrainDepth, "terrain.vert", "depth.frag");
|
||||
ld(billboard, "billboard.vert", "billboard.frag");
|
||||
ld(mesh, "mesh.vert", "mesh.frag");
|
||||
ld(meshDepth, "mesh.vert", "mesh_depth.frag");
|
||||
ld(water, "water.vert", "water.frag");
|
||||
ld(sky, "fullscreen.vert", "sky.frag");
|
||||
ld(volumetric, "fullscreen.vert", "volumetric.frag");
|
||||
ld(fogComposite, "fullscreen.vert", "fog_composite.frag");
|
||||
ld(particles, "particles.vert", "particles.frag");
|
||||
ld(dofPrep, "fullscreen.vert", "dof_prep.frag");
|
||||
ld(dofBlur, "fullscreen.vert", "dof_blur.frag");
|
||||
ld(dofComposite, "fullscreen.vert", "dof_composite.frag");
|
||||
ld(bloomDown, "fullscreen.vert", "bloom_down.frag");
|
||||
ld(bloomUp, "fullscreen.vert", "bloom_up.frag");
|
||||
ld(tonemap, "fullscreen.vert", "tonemap.frag");
|
||||
ld(ssao, "fullscreen.vert", "ssao.frag");
|
||||
ld(ssaoBlur, "fullscreen.vert", "ssao_blur.frag");
|
||||
ld(ssaoApply, "fullscreen.vert", "ssao_apply.frag");
|
||||
ld(rainStreaks, "rain.vert", "rain.frag");
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool reloadAll() {
|
||||
Shader* all[] = {&terrain, &terrainDepth, &billboard, &mesh, &meshDepth, &water,
|
||||
&sky, &volumetric, &fogComposite, &particles, &dofPrep, &dofBlur,
|
||||
&dofComposite, &bloomDown, &bloomUp, &tonemap, &ssao, &ssaoBlur,
|
||||
&ssaoApply, &rainStreaks};
|
||||
bool ok = true;
|
||||
for (Shader* s : all) ok &= s->reload();
|
||||
return ok;
|
||||
}
|
||||
|
||||
void createShadow(int size) {
|
||||
shadow.create(size, size, {}, GL_DEPTH_COMPONENT32F);
|
||||
GLuint d = shadow.depth();
|
||||
glTextureParameteri(d, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
|
||||
glTextureParameteri(d, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
|
||||
glTextureParameteri(d, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTextureParameteri(d, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
shadowSize = size;
|
||||
}
|
||||
|
||||
void createTargets(int width, int height) {
|
||||
w = std::max(1, width);
|
||||
h = std::max(1, height);
|
||||
int hw = std::max(1, w / 2), hh = std::max(1, h / 2);
|
||||
hdr.create(w, h, {GL_RGBA16F}, GL_DEPTH_COMPONENT32F);
|
||||
post.create(w, h, {GL_RGBA16F});
|
||||
sceneCopy.create(w, h, {GL_RGBA16F});
|
||||
depthCopy.create(w, h, {}, GL_DEPTH_COMPONENT32F);
|
||||
reflection.create(hw, hh, {GL_RGBA16F}, GL_DEPTH_COMPONENT32F);
|
||||
volume.create(hw, hh, {GL_RGBA16F});
|
||||
dofHalf.create(hw, hh, {GL_RGBA16F});
|
||||
dofBlurred.create(hw, hh, {GL_RGBA16F});
|
||||
ssaoRaw.create(hw, hh, {GL_R8});
|
||||
ssaoBlurred.create(hw, hh, {GL_R8});
|
||||
dofOut.create(w, h, {GL_RGBA16F});
|
||||
for (int i = 0; i < kBloomMips; ++i)
|
||||
bloom[i].create(std::max(1, w >> (i + 1)), std::max(1, h >> (i + 1)), {GL_R11F_G11F_B10F});
|
||||
|
||||
if (postDepthFbo) glDeleteFramebuffers(1, &postDepthFbo);
|
||||
glCreateFramebuffers(1, &postDepthFbo);
|
||||
glNamedFramebufferTexture(postDepthFbo, GL_COLOR_ATTACHMENT0, post.color(), 0);
|
||||
glNamedFramebufferTexture(postDepthFbo, GL_DEPTH_ATTACHMENT, hdr.depth(), 0);
|
||||
}
|
||||
|
||||
void setCamera(const glm::mat4& view, const glm::mat4& proj, const glm::vec3& camPos, int tw, int th) {
|
||||
frame.view = view;
|
||||
frame.proj = proj;
|
||||
frame.viewProj = proj * view;
|
||||
frame.invViewProj = glm::inverse(frame.viewProj);
|
||||
frame.camPos = glm::vec4(camPos, frame.camPos.w);
|
||||
frame.screen = glm::vec4(float(tw), float(th), 1.0f / float(tw), 1.0f / float(th));
|
||||
}
|
||||
void upload() { glNamedBufferSubData(ubo, 0, sizeof(FrameUBO), &frame); }
|
||||
void fullscreen() {
|
||||
glBindVertexArray(emptyVao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
}
|
||||
|
||||
void drawTerrain(const Scene& scene, Shader& s) {
|
||||
if (!scene.terrain) return;
|
||||
const Terrain& t = *scene.terrain;
|
||||
s.use();
|
||||
s.set("uMapSize", glm::ivec2(t.width(), t.depth()));
|
||||
char name[32];
|
||||
for (int m = 0; m < Terrain::kMaxMaterials; ++m) {
|
||||
const TerrainMaterial& mat = t.materials[m];
|
||||
std::snprintf(name, sizeof name, "uMatTop[%d]", m);
|
||||
s.set(name, glm::ivec4(mat.topFirst, mat.topCount, mat.accentFirst, mat.accentCount));
|
||||
std::snprintf(name, sizeof name, "uMatAccent[%d]", m);
|
||||
s.set(name, mat.accentChance);
|
||||
}
|
||||
t.textures.bind(0);
|
||||
t.bindMaterialMap(1);
|
||||
glEnable(GL_CULL_FACE);
|
||||
t.draw();
|
||||
glDisable(GL_CULL_FACE);
|
||||
}
|
||||
|
||||
void drawBillboards(const Scene& scene, int pass, bool reflectionPass) {
|
||||
billboard.use();
|
||||
billboard.set("uPass", pass);
|
||||
for (const BillboardBatch* b : scene.billboards) {
|
||||
if (!b) continue;
|
||||
if (pass == 1 && !b->castShadows) continue;
|
||||
if (reflectionPass && !b->inReflections) continue;
|
||||
billboard.set("uOpaque", b->texture.opaqueBounds());
|
||||
billboard.set("uEmissive", b->emissive);
|
||||
b->texture.bind(0);
|
||||
b->draw();
|
||||
}
|
||||
}
|
||||
|
||||
// pass 0 = color, 1 = shadow depth.
|
||||
void drawModels(const Scene& scene, int pass, bool reflectionPass) {
|
||||
Shader& s = pass == 1 ? meshDepth : mesh;
|
||||
s.use();
|
||||
if (pass == 0) s.set("uOccluderFade", reflectionPass || !settings->occluderFade ? 0 : 1);
|
||||
for (const ModelBatch* b : scene.models) {
|
||||
if (!b || !b->model) continue;
|
||||
if (pass == 1 && !b->castShadows) continue;
|
||||
if (reflectionPass && !b->inReflections) continue;
|
||||
s.set("uWindScale", b->windScale);
|
||||
for (size_t i = 0; i < b->model->primitives.size(); ++i) {
|
||||
const ModelMaterial& m = *b->model->primitives[i].material;
|
||||
s.set("uAlphaMask", m.alphaMask ? 1 : 0);
|
||||
s.set("uCutoff", m.alphaCutoff);
|
||||
if (pass == 0) {
|
||||
s.set("uBaseColor", m.baseColor);
|
||||
s.set("uFoliage", m.foliage ? 1 : 0);
|
||||
s.set("uEmissive", m.emissive);
|
||||
glBindTextureUnit(1, m.emissiveTexture ? m.emissiveTexture->id() : whiteTex);
|
||||
}
|
||||
if (m.texture) m.texture->bind(0);
|
||||
else glBindTextureUnit(0, whiteTex);
|
||||
if (m.doubleSided || pass == 1) glDisable(GL_CULL_FACE); // shadows: both faces close leaks
|
||||
else glEnable(GL_CULL_FACE);
|
||||
b->drawPrimitive(i);
|
||||
}
|
||||
}
|
||||
glDisable(GL_CULL_FACE);
|
||||
}
|
||||
};
|
||||
|
||||
Renderer::Renderer() : impl_(std::make_unique<Impl>()) {}
|
||||
Renderer::~Renderer() = default;
|
||||
|
||||
bool Renderer::init(const std::string& shaderDir, int width, int height) {
|
||||
Impl& I = *impl_;
|
||||
I.dir = shaderDir;
|
||||
I.settings = &settings;
|
||||
if (!I.loadShaders()) return false;
|
||||
glCreateBuffers(1, &I.ubo);
|
||||
glNamedBufferData(I.ubo, sizeof(FrameUBO), nullptr, GL_DYNAMIC_DRAW);
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, 0, I.ubo);
|
||||
glCreateVertexArrays(1, &I.emptyVao);
|
||||
{
|
||||
const unsigned char white[4] = {255, 255, 255, 255};
|
||||
glCreateTextures(GL_TEXTURE_2D, 1, &I.whiteTex);
|
||||
glTextureStorage2D(I.whiteTex, 1, GL_RGBA8, 1, 1);
|
||||
glTextureSubImage2D(I.whiteTex, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, white);
|
||||
}
|
||||
glCreateQueries(GL_TIME_ELAPSED, 2, I.timers);
|
||||
I.createTargets(width, height);
|
||||
I.createShadow(settings.shadowMapSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Renderer::resize(int width, int height) {
|
||||
if (width > 0 && height > 0) impl_->createTargets(width, height);
|
||||
}
|
||||
|
||||
bool Renderer::reloadShaders() {
|
||||
bool ok = impl_->reloadAll();
|
||||
HD2D_LOG("shader reload %s", ok ? "ok" : "had errors (kept previous programs)");
|
||||
return ok;
|
||||
}
|
||||
|
||||
void Renderer::render(const Scene& scene, const Camera& cam, float time) {
|
||||
Impl& I = *impl_;
|
||||
const RenderSettings& S = settings;
|
||||
FrameUBO& f = I.frame;
|
||||
const int w = I.w, h = I.h, hw = I.volume.width(), hh = I.volume.height();
|
||||
|
||||
// GPU timing (read the query from two frames ago to avoid stalls).
|
||||
int qi = I.timerIdx;
|
||||
if (I.timerUsed[qi]) {
|
||||
GLuint64 ns = 0;
|
||||
glGetQueryObjectui64v(I.timers[qi], GL_QUERY_RESULT, &ns);
|
||||
gpuMs_ = float(double(ns) * 1e-6);
|
||||
}
|
||||
glBeginQuery(GL_TIME_ELAPSED, I.timers[qi]);
|
||||
|
||||
if (S.shadowMapSize != I.shadowSize) I.createShadow(S.shadowMapSize);
|
||||
|
||||
const float aspect = float(w) / float(h);
|
||||
const glm::mat4 view = cam.view(), proj = cam.projection(aspect);
|
||||
const glm::vec3 camPos = cam.position();
|
||||
const glm::vec3 sunDir = scene.sun.direction();
|
||||
|
||||
// Sun shadow frustum follows the camera target, snapped to texels so
|
||||
// shadows don't shimmer while moving.
|
||||
const float E = S.shadowExtent;
|
||||
glm::vec3 up = std::abs(sunDir.y) > 0.99f ? glm::vec3(0, 0, 1) : glm::vec3(0, 1, 0);
|
||||
glm::mat4 lightView = glm::lookAt(cam.target + sunDir * 80.0f, cam.target, up);
|
||||
glm::mat4 lightProj = glm::ortho(-E, E, -E, E, 1.0f, 200.0f);
|
||||
{
|
||||
glm::vec4 o = lightProj * lightView * glm::vec4(0, 0, 0, 1);
|
||||
float half = float(I.shadowSize) * 0.5f;
|
||||
glm::vec2 p = glm::vec2(o) * half;
|
||||
glm::vec2 off = (glm::round(p) - p) / half;
|
||||
lightProj[3][0] += off.x;
|
||||
lightProj[3][1] += off.y;
|
||||
}
|
||||
const glm::mat4 lightVP = lightProj * lightView;
|
||||
|
||||
glm::vec3 shadowRight(-sunDir.z, 0.0f, sunDir.x);
|
||||
if (glm::length(shadowRight) < 1e-3f) shadowRight = cam.right();
|
||||
shadowRight = glm::normalize(shadowRight);
|
||||
if (glm::dot(shadowRight, cam.right()) < 0.0f) shadowRight = -shadowRight;
|
||||
|
||||
f.lightViewProj = lightVP;
|
||||
f.camPos.w = time;
|
||||
f.camParams = glm::vec4(cam.nearPlane, cam.farPlane, glm::length(scene.focusPoint - camPos), 0.0f);
|
||||
f.sunDir = glm::vec4(sunDir, scene.sun.intensity);
|
||||
f.sunColor = glm::vec4(scene.sun.color, 0.0f);
|
||||
f.skyZenith = glm::vec4(scene.sky.zenith, scene.sky.ambient);
|
||||
f.skyHorizon = glm::vec4(scene.sky.horizon, scene.sky.fill);
|
||||
f.skyGround = glm::vec4(scene.sky.ground, 0.0f);
|
||||
f.fogColor = glm::vec4(S.fogColor, S.volumetrics ? S.fogDensity : 0.0f);
|
||||
f.fogParams = glm::vec4(S.fogHeightFalloff, S.fogBaseHeight, S.fogNoise, S.fogAnisotropy);
|
||||
f.fogParams2 = glm::vec4(S.godRayIntensity, S.fogMaxDistance, float(std::max(4, S.fogSteps)), 0.0f);
|
||||
glm::vec2 wd = glm::length(S.windDirection) > 1e-4f ? glm::normalize(S.windDirection) : glm::vec2(1, 0);
|
||||
f.wind = glm::vec4(wd, S.windStrength, S.windGustSpeed);
|
||||
f.interactor = glm::vec4(scene.interactor, scene.interactorRadius);
|
||||
f.blob = glm::vec4(scene.blobShadowPos, scene.blobShadowRadius);
|
||||
f.clipPlane = glm::vec4(0, 0, 0, 1);
|
||||
f.billboardRight = glm::vec4(cam.right(), 0.0f);
|
||||
f.shadowRight = glm::vec4(shadowRight, 0.0f);
|
||||
f.cookie = glm::vec4(S.cookieStrength, S.cookieScale, S.cookieSpeed, S.cookieCoverage);
|
||||
f.shadowParams = glm::vec4(1.0f / float(I.shadowSize), S.shadowSoftness, 0, 0);
|
||||
f.shaft = glm::vec4(S.shaftStrength, S.shaftCoverage, S.shaftScale, 0);
|
||||
f.waterParams = scene.water ? glm::vec4(scene.water->level(), scene.water->causticsIntensity, 1, 0)
|
||||
: 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);
|
||||
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);
|
||||
for (int i = 0; i < nl; ++i) {
|
||||
f.lightPos[i] = glm::vec4(scene.lights[i].position, scene.lights[i].radius);
|
||||
f.lightColor[i] = glm::vec4(scene.lights[i].color, 0.0f);
|
||||
}
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
glDepthMask(GL_TRUE);
|
||||
|
||||
// ---- 1. Sun shadow map --------------------------------------------------
|
||||
glBindTextureUnit(8, 0);
|
||||
I.setCamera(lightView, lightProj, cam.target + sunDir * 80.0f, I.shadowSize, I.shadowSize);
|
||||
I.upload();
|
||||
I.shadow.bind();
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
glEnable(GL_POLYGON_OFFSET_FILL);
|
||||
glPolygonOffset(1.5f, 3.0f);
|
||||
if (scene.terrain) {
|
||||
I.terrainDepth.use();
|
||||
scene.terrain->draw(); // both faces: closes light leaks at cliff edges
|
||||
}
|
||||
I.drawBillboards(scene, 1, false);
|
||||
I.drawModels(scene, 1, false);
|
||||
glDisable(GL_POLYGON_OFFSET_FILL);
|
||||
glBindTextureUnit(8, I.shadow.depth());
|
||||
|
||||
// ---- 2. Planar reflection (half res, mirrored about the water plane) ---
|
||||
const bool doReflection = scene.water && S.reflections;
|
||||
if (doReflection) {
|
||||
const float L = scene.water->level();
|
||||
glm::mat4 R = glm::translate(glm::mat4(1.0f), glm::vec3(0, 2.0f * L, 0)) *
|
||||
glm::scale(glm::mat4(1.0f), glm::vec3(1, -1, 1));
|
||||
I.setCamera(view * R, proj, glm::vec3(camPos.x, 2.0f * L - camPos.y, camPos.z), hw, hh);
|
||||
f.clipPlane = glm::vec4(0, 1, 0, -(L - 0.05f));
|
||||
I.upload();
|
||||
I.reflection.bind();
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
glEnable(GL_CLIP_DISTANCE0);
|
||||
glFrontFace(GL_CW); // mirroring flips winding
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
I.sky.use();
|
||||
I.fullscreen();
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
I.drawTerrain(scene, I.terrain);
|
||||
I.drawBillboards(scene, 0, true);
|
||||
I.drawModels(scene, 0, true);
|
||||
glFrontFace(GL_CCW);
|
||||
glDisable(GL_CLIP_DISTANCE0);
|
||||
f.clipPlane = glm::vec4(0, 0, 0, 1);
|
||||
}
|
||||
|
||||
// ---- 3. Main HDR scene ----------------------------------------------
|
||||
I.setCamera(view, proj, camPos, w, h);
|
||||
I.upload();
|
||||
I.hdr.bind();
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
I.sky.use();
|
||||
I.fullscreen();
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
I.drawTerrain(scene, I.terrain);
|
||||
I.drawModels(scene, 0, false); // opaque models first: early-z for the sprites behind them
|
||||
I.drawBillboards(scene, 0, false);
|
||||
|
||||
// ---- 3b. SSAO (half res) multiplied into the opaque scene ---------------
|
||||
// Runs before water so the shore and refracted bottom get AO but the water
|
||||
// surface does not.
|
||||
if (S.ssao) {
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glm::vec2 halfTexel(1.0f / float(hw), 1.0f / float(hh));
|
||||
I.ssaoRaw.bind();
|
||||
I.ssao.use();
|
||||
I.ssao.set("uRadius", S.ssaoRadius);
|
||||
I.ssao.set("uIntensity", S.ssaoIntensity);
|
||||
glBindTextureUnit(0, I.hdr.depth());
|
||||
I.fullscreen();
|
||||
I.ssaoBlur.use();
|
||||
for (int pass = 0; pass < 2; ++pass) { // separable depth-aware blur
|
||||
(pass == 0 ? I.ssaoBlurred : I.ssaoRaw).bind();
|
||||
I.ssaoBlur.set("uDir", pass == 0 ? glm::vec2(halfTexel.x, 0) : glm::vec2(0, halfTexel.y));
|
||||
glBindTextureUnit(0, pass == 0 ? I.ssaoRaw.color() : I.ssaoBlurred.color());
|
||||
glBindTextureUnit(1, I.hdr.depth());
|
||||
I.fullscreen();
|
||||
}
|
||||
// color *= ao, via blending (no extra copy of the HDR target).
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, I.hdr.fbo());
|
||||
glViewport(0, 0, w, h);
|
||||
glDepthMask(GL_FALSE);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_DST_COLOR, GL_ZERO);
|
||||
I.ssaoApply.use();
|
||||
I.ssaoApply.set("uAoTexel", halfTexel);
|
||||
glBindTextureUnit(0, I.ssaoRaw.color());
|
||||
glBindTextureUnit(1, I.hdr.depth());
|
||||
I.fullscreen();
|
||||
glDisable(GL_BLEND);
|
||||
glDepthMask(GL_TRUE);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
// ---- 4. Water (reads copies of opaque color + depth) -------------------
|
||||
if (scene.water) {
|
||||
glBlitNamedFramebuffer(I.hdr.fbo(), I.sceneCopy.fbo(), 0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glBlitNamedFramebuffer(I.hdr.fbo(), I.depthCopy.fbo(), 0, 0, w, h, 0, 0, w, h, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
||||
const Water& wt = *scene.water;
|
||||
Shader& s = I.water;
|
||||
s.use();
|
||||
s.set("uShallow", wt.shallowColor);
|
||||
s.set("uDeep", wt.deepColor);
|
||||
s.set("uAbsorb", wt.absorption);
|
||||
s.set("uClarity", wt.clarity);
|
||||
s.set("uWaveScale", wt.waveScale);
|
||||
s.set("uWaveStrength", wt.waveStrength);
|
||||
s.set("uWaveSpeed", wt.waveSpeed);
|
||||
s.set("uRefraction", wt.refraction);
|
||||
s.set("uReflDistort", wt.reflectionDistort);
|
||||
s.set("uSpecular", wt.specular);
|
||||
s.set("uFoamWidth", wt.foamWidth);
|
||||
s.set("uFoamIntensity", wt.foamIntensity);
|
||||
s.set("uGlitter", wt.glitter);
|
||||
s.set("uShoreRect", wt.shoreRect());
|
||||
s.set("uHasReflection", doReflection ? 1 : 0);
|
||||
glBindTextureUnit(0, I.reflection.color());
|
||||
glBindTextureUnit(1, I.sceneCopy.color());
|
||||
glBindTextureUnit(2, I.depthCopy.depth());
|
||||
glBindTextureUnit(3, wt.shoreTexture());
|
||||
I.hdr.bind();
|
||||
wt.draw();
|
||||
}
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
|
||||
// ---- 5. Volumetric fog / god rays (half res) ---------------------------
|
||||
if (S.volumetrics) {
|
||||
I.volume.bind();
|
||||
I.volumetric.use();
|
||||
glBindTextureUnit(0, I.hdr.depth());
|
||||
I.fullscreen();
|
||||
}
|
||||
I.post.bind();
|
||||
I.fogComposite.use();
|
||||
I.fogComposite.set("uVolumeTexel", glm::vec2(1.0f / float(hw), 1.0f / float(hh)));
|
||||
I.fogComposite.set("uEnabled", S.volumetrics ? 1 : 0);
|
||||
glBindTextureUnit(0, I.hdr.color());
|
||||
glBindTextureUnit(1, I.hdr.depth());
|
||||
glBindTextureUnit(2, I.volume.color());
|
||||
I.fullscreen();
|
||||
|
||||
// ---- 5b. Rain streaks (procedural, depth tested) -------------------------
|
||||
if (scene.rain > 0.001f && S.maxRainDrops > 0) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, I.postDepthFbo);
|
||||
glViewport(0, 0, w, h);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthMask(GL_FALSE);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
I.rainStreaks.use();
|
||||
I.rainStreaks.set("uRainCenter", cam.target);
|
||||
I.rainStreaks.set("uRainBox", glm::vec2(16.0f, 14.0f));
|
||||
glBindVertexArray(I.emptyVao);
|
||||
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, GLsizei(float(S.maxRainDrops) * scene.rain));
|
||||
glDisable(GL_BLEND);
|
||||
glDepthMask(GL_TRUE);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
// ---- 6. Particles (depth tested, no depth writes) ----------------------
|
||||
if (scene.particles) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, I.postDepthFbo);
|
||||
glViewport(0, 0, w, h);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDepthMask(GL_FALSE);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
|
||||
I.particles.use();
|
||||
scene.particles->draw();
|
||||
glDisable(GL_BLEND);
|
||||
glDepthMask(GL_TRUE);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
// ---- 7. Depth of field ---------------------------------------------------
|
||||
GLuint finalHdr = I.post.color();
|
||||
if (S.dof) {
|
||||
glm::vec4 dofParams(f.camParams.z, S.focusRange, S.focusTransition, S.dofMaxRadius);
|
||||
I.dofHalf.bind();
|
||||
I.dofPrep.use();
|
||||
I.dofPrep.set("uDof", dofParams);
|
||||
I.dofPrep.set("uTiltShift", S.tiltShift);
|
||||
glBindTextureUnit(0, I.post.color());
|
||||
glBindTextureUnit(1, I.hdr.depth());
|
||||
I.fullscreen();
|
||||
|
||||
I.dofBlurred.bind();
|
||||
I.dofBlur.use();
|
||||
I.dofBlur.set("uMaxRadius", S.dofMaxRadius);
|
||||
I.dofBlur.set("uTexel", glm::vec2(1.0f / float(hw), 1.0f / float(hh)));
|
||||
glBindTextureUnit(0, I.dofHalf.color());
|
||||
I.fullscreen();
|
||||
|
||||
I.dofOut.bind();
|
||||
I.dofComposite.use();
|
||||
I.dofComposite.set("uDof", dofParams);
|
||||
I.dofComposite.set("uTiltShift", S.tiltShift);
|
||||
glBindTextureUnit(0, I.post.color());
|
||||
glBindTextureUnit(1, I.dofBlurred.color());
|
||||
glBindTextureUnit(2, I.hdr.depth());
|
||||
I.fullscreen();
|
||||
finalHdr = I.dofOut.color();
|
||||
}
|
||||
|
||||
// ---- 8. Bloom ------------------------------------------------------------
|
||||
if (S.bloom) {
|
||||
GLuint src = finalHdr;
|
||||
glm::vec2 srcSize(w, h);
|
||||
I.bloomDown.use();
|
||||
for (int i = 0; i < kBloomMips; ++i) {
|
||||
I.bloom[i].bind();
|
||||
I.bloomDown.set("uSrcTexel", 1.0f / srcSize);
|
||||
I.bloomDown.set("uKaris", i == 0 ? 1 : 0);
|
||||
glBindTextureUnit(0, src);
|
||||
I.fullscreen();
|
||||
src = I.bloom[i].color();
|
||||
srcSize = glm::vec2(I.bloom[i].width(), I.bloom[i].height());
|
||||
}
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_ONE, GL_ONE);
|
||||
I.bloomUp.use();
|
||||
I.bloomUp.set("uRadius", S.bloomRadius);
|
||||
for (int i = kBloomMips - 1; i > 0; --i) {
|
||||
I.bloom[i - 1].bind();
|
||||
I.bloomUp.set("uSrcTexel", glm::vec2(1.0f / float(I.bloom[i].width()), 1.0f / float(I.bloom[i].height())));
|
||||
glBindTextureUnit(0, I.bloom[i].color());
|
||||
I.fullscreen();
|
||||
}
|
||||
glDisable(GL_BLEND);
|
||||
}
|
||||
|
||||
// ---- 9. Tonemap + grade to the backbuffer --------------------------------
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glViewport(0, 0, w, h);
|
||||
Shader& tm = I.tonemap;
|
||||
tm.use();
|
||||
tm.set("uBloomStrength", S.bloom ? S.bloomStrength : 0.0f);
|
||||
tm.set("uBloomNorm", 1.0f / float(kBloomMips)); // upsample chain sums every mip
|
||||
tm.set("uExposure", S.exposure);
|
||||
tm.set("uContrast", S.contrast);
|
||||
tm.set("uSaturation", S.saturation);
|
||||
tm.set("uLift", S.lift);
|
||||
tm.set("uGain", S.gain);
|
||||
tm.set("uVignette", S.vignette);
|
||||
tm.set("uChromatic", S.chromaticAberration);
|
||||
tm.set("uGrain", S.grain);
|
||||
tm.set("uTimeSeed", std::fmod(time * 61.0f, 1000.0f));
|
||||
glBindTextureUnit(0, finalHdr);
|
||||
glBindTextureUnit(1, I.bloom[0].color());
|
||||
I.fullscreen();
|
||||
|
||||
glEndQuery(GL_TIME_ELAPSED);
|
||||
I.timerUsed[qi] = true;
|
||||
I.timerIdx = 1 - qi;
|
||||
}
|
||||
|
||||
} // namespace hd2d
|
||||
31
engine/src/Screenshot.cpp
Normal file
31
engine/src/Screenshot.cpp
Normal file
@@ -0,0 +1,31 @@
|
||||
#include "hd2d/Screenshot.h"
|
||||
|
||||
#include "hd2d/GL.h"
|
||||
#include "hd2d/Log.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include <stb_image_write.h>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
bool saveScreenshot(const std::string& path, int width, int height) {
|
||||
std::vector<unsigned char> px(size_t(width) * height * 3);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||
glReadBuffer(GL_BACK);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, px.data());
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
std::error_code ec;
|
||||
auto parent = std::filesystem::path(path).parent_path();
|
||||
if (!parent.empty()) std::filesystem::create_directories(parent, ec);
|
||||
stbi_flip_vertically_on_write(1);
|
||||
bool ok = stbi_write_png(path.c_str(), width, height, 3, px.data(), width * 3) != 0;
|
||||
if (ok) HD2D_LOG("screenshot saved: %s", path.c_str());
|
||||
else HD2D_ERR("failed to write screenshot '%s'", path.c_str());
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace hd2d
|
||||
131
engine/src/Shader.cpp
Normal file
131
engine/src/Shader.cpp
Normal file
@@ -0,0 +1,131 @@
|
||||
#include "hd2d/Shader.h"
|
||||
|
||||
#include "hd2d/Log.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static bool readFile(const fs::path& path, std::string& out) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f) return false;
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
out = ss.str();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Expands #include "x" lines recursively; each file is included at most once.
|
||||
static bool preprocess(const fs::path& path, std::string& out, std::unordered_set<std::string>& seen) {
|
||||
std::string src;
|
||||
if (!readFile(path, src)) {
|
||||
HD2D_ERR("cannot read shader '%s'", path.string().c_str());
|
||||
return false;
|
||||
}
|
||||
std::istringstream in(src);
|
||||
std::string line;
|
||||
while (std::getline(in, line)) {
|
||||
auto pos = line.find("#include");
|
||||
if (pos != std::string::npos && line.find_first_not_of(" \t") == pos) {
|
||||
auto a = line.find('"', pos), b = line.rfind('"');
|
||||
if (a == std::string::npos || b <= a) {
|
||||
HD2D_ERR("%s: malformed #include", path.string().c_str());
|
||||
return false;
|
||||
}
|
||||
fs::path inc = path.parent_path() / line.substr(a + 1, b - a - 1);
|
||||
std::string key = fs::weakly_canonical(inc).string();
|
||||
if (seen.insert(key).second && !preprocess(inc, out, seen)) return false;
|
||||
out += "\n";
|
||||
continue;
|
||||
}
|
||||
out += line;
|
||||
out += '\n';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static GLuint compile(GLenum type, const std::string& path) {
|
||||
std::string src;
|
||||
std::unordered_set<std::string> seen;
|
||||
if (!preprocess(path, src, seen)) return 0;
|
||||
GLuint s = glCreateShader(type);
|
||||
const char* p = src.c_str();
|
||||
glShaderSource(s, 1, &p, nullptr);
|
||||
glCompileShader(s);
|
||||
GLint ok = 0;
|
||||
glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char log[8192];
|
||||
glGetShaderInfoLog(s, sizeof log, nullptr, log);
|
||||
HD2D_ERR("compile '%s':\n%s", path.c_str(), log);
|
||||
glDeleteShader(s);
|
||||
return 0;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Shader::~Shader() {
|
||||
if (program_) glDeleteProgram(program_);
|
||||
}
|
||||
|
||||
bool Shader::load(const std::string& vertPath, const std::string& fragPath) {
|
||||
vertPath_ = vertPath;
|
||||
fragPath_ = fragPath;
|
||||
return reload();
|
||||
}
|
||||
|
||||
bool Shader::reload() {
|
||||
GLuint vs = compile(GL_VERTEX_SHADER, vertPath_);
|
||||
GLuint fs = compile(GL_FRAGMENT_SHADER, fragPath_);
|
||||
if (!vs || !fs) {
|
||||
if (vs) glDeleteShader(vs);
|
||||
if (fs) glDeleteShader(fs);
|
||||
return false;
|
||||
}
|
||||
GLuint prog = glCreateProgram();
|
||||
glAttachShader(prog, vs);
|
||||
glAttachShader(prog, fs);
|
||||
glLinkProgram(prog);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
GLint ok = 0;
|
||||
glGetProgramiv(prog, GL_LINK_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char log[8192];
|
||||
glGetProgramInfoLog(prog, sizeof log, nullptr, log);
|
||||
HD2D_ERR("link '%s' + '%s':\n%s", vertPath_.c_str(), fragPath_.c_str(), log);
|
||||
glDeleteProgram(prog);
|
||||
return false;
|
||||
}
|
||||
if (program_) glDeleteProgram(program_);
|
||||
program_ = prog;
|
||||
locations_.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
GLint Shader::location(const char* name) {
|
||||
auto it = locations_.find(name);
|
||||
if (it != locations_.end()) return it->second;
|
||||
GLint loc = glGetUniformLocation(program_, name);
|
||||
locations_.emplace(name, loc);
|
||||
return loc;
|
||||
}
|
||||
|
||||
void Shader::set(const char* n, int v) { glProgramUniform1i(program_, location(n), v); }
|
||||
void Shader::set(const char* n, float v) { glProgramUniform1f(program_, location(n), v); }
|
||||
void Shader::set(const char* n, const glm::vec2& v) { glProgramUniform2fv(program_, location(n), 1, glm::value_ptr(v)); }
|
||||
void Shader::set(const char* n, const glm::vec3& v) { glProgramUniform3fv(program_, location(n), 1, glm::value_ptr(v)); }
|
||||
void Shader::set(const char* n, const glm::vec4& v) { glProgramUniform4fv(program_, location(n), 1, glm::value_ptr(v)); }
|
||||
void Shader::set(const char* n, const glm::ivec2& v) { glProgramUniform2iv(program_, location(n), 1, glm::value_ptr(v)); }
|
||||
void Shader::set(const char* n, const glm::mat4& v) {
|
||||
glProgramUniformMatrix4fv(program_, location(n), 1, GL_FALSE, glm::value_ptr(v));
|
||||
}
|
||||
void Shader::set(const char* n, const glm::ivec4& v) { glProgramUniform4iv(program_, location(n), 1, glm::value_ptr(v)); }
|
||||
|
||||
} // namespace hd2d
|
||||
150
engine/src/Terrain.cpp
Normal file
150
engine/src/Terrain.cpp
Normal file
@@ -0,0 +1,150 @@
|
||||
#include "hd2d/Terrain.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Vertex {
|
||||
glm::vec3 pos;
|
||||
glm::vec3 normal;
|
||||
glm::vec2 uv;
|
||||
float layer; // texture layer (side faces only; top faces resolve in shader)
|
||||
float ao;
|
||||
float kind; // 0 = top, 1 = side, 2 + n = side with an overhang lip from layer n
|
||||
};
|
||||
|
||||
uint32_t hash3(int x, int y, int z) {
|
||||
uint32_t h = uint32_t(x) * 73856093u ^ uint32_t(y) * 19349663u ^ uint32_t(z) * 83492791u;
|
||||
h ^= h >> 13;
|
||||
h *= 0x5bd1e995u;
|
||||
h ^= h >> 15;
|
||||
return h;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Terrain::init(int width, int depth, float levelHeight) {
|
||||
w_ = width;
|
||||
d_ = depth;
|
||||
levelHeight_ = levelHeight;
|
||||
levels_.assign(size_t(w_) * d_, 0);
|
||||
mats_.assign(size_t(w_) * d_, 0);
|
||||
}
|
||||
|
||||
int Terrain::level(int x, int z) const { return inBounds(x, z) ? levels_[size_t(z) * w_ + x] : baseLevel; }
|
||||
uint8_t Terrain::material(int x, int z) const { return inBounds(x, z) ? mats_[size_t(z) * w_ + x] : 0; }
|
||||
void Terrain::setLevel(int x, int z, int l) {
|
||||
if (inBounds(x, z)) levels_[size_t(z) * w_ + x] = l;
|
||||
}
|
||||
void Terrain::setMaterial(int x, int z, uint8_t m) {
|
||||
if (inBounds(x, z)) mats_[size_t(z) * w_ + x] = m;
|
||||
}
|
||||
float Terrain::heightAt(float x, float z) const {
|
||||
return float(level(int(std::floor(x)), int(std::floor(z)))) * levelHeight_;
|
||||
}
|
||||
|
||||
void Terrain::build() {
|
||||
std::vector<Vertex> verts;
|
||||
std::vector<uint32_t> idx;
|
||||
verts.reserve(size_t(w_) * d_ * 8);
|
||||
|
||||
auto quad = [&](const Vertex& a, const Vertex& b, const Vertex& c, const Vertex& d, bool flip) {
|
||||
uint32_t base = uint32_t(verts.size());
|
||||
verts.insert(verts.end(), {a, b, c, d});
|
||||
// Flip the diagonal so AO interpolates without anisotropy artifacts.
|
||||
if (flip) idx.insert(idx.end(), {base + 1, base + 2, base + 3, base + 1, base + 3, base + 0});
|
||||
else idx.insert(idx.end(), {base, base + 1, base + 2, base, base + 2, base + 3});
|
||||
};
|
||||
|
||||
const int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
|
||||
for (int z = 0; z < d_; ++z)
|
||||
for (int x = 0; x < w_; ++x) {
|
||||
const int L = level(x, z);
|
||||
const float top = float(L) * levelHeight_;
|
||||
const TerrainMaterial& mat = materials[std::min<int>(material(x, z), kMaxMaterials - 1)];
|
||||
|
||||
// Top face with per-corner ambient occlusion from higher neighbours.
|
||||
auto higher = [&](int nx, int nz) { return inBounds(nx, nz) && level(nx, nz) > L ? 1 : 0; };
|
||||
auto cornerAO = [&](int cx, int cz) {
|
||||
int sx = cx ? 1 : -1, sz = cz ? 1 : -1;
|
||||
int s1 = higher(x + sx, z), s2 = higher(x, z + sz), c = higher(x + sx, z + sz);
|
||||
int ao = (s1 && s2) ? 0 : 3 - (s1 + s2 + c);
|
||||
return 0.45f + 0.55f * float(ao) / 3.0f;
|
||||
};
|
||||
Vertex v0{{x, top, z}, {0, 1, 0}, {x, z}, 0, cornerAO(0, 0), 0};
|
||||
Vertex v1{{x, top, z + 1}, {0, 1, 0}, {x, z + 1}, 0, cornerAO(0, 1), 0};
|
||||
Vertex v2{{x + 1, top, z + 1}, {0, 1, 0}, {x + 1, z + 1}, 0, cornerAO(1, 1), 0};
|
||||
Vertex v3{{x + 1, top, z}, {0, 1, 0}, {x + 1, z}, 0, cornerAO(1, 0), 0};
|
||||
quad(v0, v1, v2, v3, v0.ao + v2.ao < v1.ao + v3.ao);
|
||||
|
||||
// Cliff faces toward lower neighbours (or the diorama base at the border).
|
||||
for (int dIdx = 0; dIdx < 4; ++dIdx) {
|
||||
int nx = x + dirs[dIdx][0], nz = z + dirs[dIdx][1];
|
||||
int nl = level(nx, nz);
|
||||
if (nl >= L) continue;
|
||||
float bottom = float(nl) * levelHeight_;
|
||||
glm::vec3 n(float(dirs[dIdx][0]), 0, float(dirs[dIdx][1]));
|
||||
glm::vec3 bl, br;
|
||||
switch (dIdx) {
|
||||
case 0: bl = {x + 1, bottom, z + 1}, br = {x + 1, bottom, z}; break;
|
||||
case 1: bl = {x, bottom, z}, br = {x, bottom, z + 1}; break;
|
||||
case 2: bl = {x, bottom, z + 1}, br = {x + 1, bottom, z + 1}; break;
|
||||
default: bl = {x + 1, bottom, z}, br = {x, bottom, z}; break;
|
||||
}
|
||||
glm::vec3 rightDir = glm::normalize(br - bl);
|
||||
float uL = glm::dot(bl, rightDir), uR = glm::dot(br, rightDir);
|
||||
float layer = float(mat.sideFirst + int(hash3(x, z, dIdx) % uint32_t(std::max(1, mat.sideCount))));
|
||||
float kind = mat.lipLayer >= 0 ? 2.0f + float(mat.lipLayer) : 1.0f;
|
||||
float height = top - bottom;
|
||||
bool groundBelow = inBounds(nx, nz);
|
||||
float aoBottom = groundBelow ? 0.55f : 0.8f;
|
||||
Vertex a{bl, n, {uL, height}, layer, aoBottom, kind};
|
||||
Vertex b{br, n, {uR, height}, layer, aoBottom, kind};
|
||||
Vertex c{{br.x, top, br.z}, n, {uR, 0}, layer, 1.0f, kind};
|
||||
Vertex d{{bl.x, top, bl.z}, n, {uL, 0}, layer, 1.0f, kind};
|
||||
quad(a, b, c, d, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!vao_) {
|
||||
glCreateVertexArrays(1, &vao_);
|
||||
glCreateBuffers(1, &vbo_);
|
||||
glCreateBuffers(1, &ibo_);
|
||||
}
|
||||
glNamedBufferData(vbo_, GLsizeiptr(verts.size() * sizeof(Vertex)), verts.data(), GL_STATIC_DRAW);
|
||||
glNamedBufferData(ibo_, GLsizeiptr(idx.size() * sizeof(uint32_t)), idx.data(), GL_STATIC_DRAW);
|
||||
indexCount_ = GLsizei(idx.size());
|
||||
|
||||
glVertexArrayVertexBuffer(vao_, 0, vbo_, 0, sizeof(Vertex));
|
||||
glVertexArrayElementBuffer(vao_, ibo_);
|
||||
auto attr = [&](GLuint loc, GLint n, size_t offset) {
|
||||
glEnableVertexArrayAttrib(vao_, loc);
|
||||
glVertexArrayAttribFormat(vao_, loc, n, GL_FLOAT, GL_FALSE, GLuint(offset));
|
||||
glVertexArrayAttribBinding(vao_, loc, 0);
|
||||
};
|
||||
attr(0, 3, offsetof(Vertex, pos));
|
||||
attr(1, 3, offsetof(Vertex, normal));
|
||||
attr(2, 2, offsetof(Vertex, uv));
|
||||
attr(3, 3, offsetof(Vertex, layer)); // layer, ao, kind
|
||||
|
||||
if (matTex_) glDeleteTextures(1, &matTex_);
|
||||
glCreateTextures(GL_TEXTURE_2D, 1, &matTex_);
|
||||
glTextureStorage2D(matTex_, 1, GL_R8UI, w_, d_);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTextureSubImage2D(matTex_, 0, 0, 0, w_, d_, GL_RED_INTEGER, GL_UNSIGNED_BYTE, mats_.data());
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glTextureParameteri(matTex_, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTextureParameteri(matTex_, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
}
|
||||
|
||||
void Terrain::draw() const {
|
||||
glBindVertexArray(vao_);
|
||||
glDrawElements(GL_TRIANGLES, indexCount_, GL_UNSIGNED_INT, nullptr);
|
||||
}
|
||||
|
||||
} // namespace hd2d
|
||||
146
engine/src/Texture.cpp
Normal file
146
engine/src/Texture.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
#include "hd2d/Texture.h"
|
||||
|
||||
#include "hd2d/Log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#define STBI_ONLY_PNG
|
||||
#include <stb_image.h>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
static 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;
|
||||
// A few dilation rounds are enough to cover any filter footprint.
|
||||
for (int pass = 0; pass < 8; ++pass) {
|
||||
std::vector<unsigned char> next = known;
|
||||
for (int y = 0; y < h; ++y)
|
||||
for (int x = 0; x < w; ++x) {
|
||||
int i = y * w + x;
|
||||
if (known[i]) continue;
|
||||
int r = 0, g = 0, b = 0, n = 0;
|
||||
for (int dy = -1; dy <= 1; ++dy)
|
||||
for (int dx = -1; dx <= 1; ++dx) {
|
||||
int nx = x + dx, ny = y + dy;
|
||||
if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
|
||||
int j = ny * w + nx;
|
||||
if (!known[j]) continue;
|
||||
r += img.rgba[j * 4], g += img.rgba[j * 4 + 1], b += img.rgba[j * 4 + 2], ++n;
|
||||
}
|
||||
if (n) {
|
||||
img.rgba[i * 4] = (unsigned char)(r / n);
|
||||
img.rgba[i * 4 + 1] = (unsigned char)(g / n);
|
||||
img.rgba[i * 4 + 2] = (unsigned char)(b / n);
|
||||
next[i] = 1;
|
||||
}
|
||||
}
|
||||
known.swap(next);
|
||||
}
|
||||
}
|
||||
|
||||
static bool finishImage(unsigned char* data, Image& out, const char* what) {
|
||||
if (!data) {
|
||||
HD2D_ERR("cannot load image '%s': %s", what, stbi_failure_reason());
|
||||
return false;
|
||||
}
|
||||
out.rgba.assign(data, data + size_t(out.width) * out.height * 4);
|
||||
stbi_image_free(data);
|
||||
bleedTransparentColors(out);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool loadImage(const std::string& path, Image& out) {
|
||||
stbi_set_flip_vertically_on_load(1);
|
||||
int n = 0;
|
||||
return finishImage(stbi_load(path.c_str(), &out.width, &out.height, &n, 4), out, path.c_str());
|
||||
}
|
||||
|
||||
bool decodeImage(const unsigned char* data, size_t size, Image& out, bool flipY) {
|
||||
stbi_set_flip_vertically_on_load(flipY ? 1 : 0);
|
||||
int n = 0;
|
||||
return finishImage(stbi_load_from_memory(data, int(size), &out.width, &out.height, &n, 4), out, "<memory>");
|
||||
}
|
||||
|
||||
static int mipLevels(int w, int h) { return 1 + int(std::floor(std::log2(float(std::max(w, h))))); }
|
||||
|
||||
Texture& Texture::operator=(Texture&& o) noexcept {
|
||||
std::swap(id_, o.id_);
|
||||
width_ = o.width_;
|
||||
height_ = o.height_;
|
||||
opaqueBounds_ = o.opaqueBounds_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Texture::~Texture() {
|
||||
if (id_) glDeleteTextures(1, &id_);
|
||||
}
|
||||
|
||||
bool Texture::load(const std::string& path, bool repeat) {
|
||||
Image img;
|
||||
return loadImage(path, img) && create(img, repeat);
|
||||
}
|
||||
|
||||
bool Texture::create(const Image& img, bool repeat, float anisotropy) {
|
||||
width_ = img.width;
|
||||
height_ = img.height;
|
||||
|
||||
int minX = width_, minY = height_, maxX = -1, maxY = -1;
|
||||
for (int y = 0; y < height_; ++y)
|
||||
for (int x = 0; x < width_; ++x)
|
||||
if (img.rgba[(size_t(y) * width_ + x) * 4 + 3] >= 128) {
|
||||
minX = std::min(minX, x), maxX = std::max(maxX, x);
|
||||
minY = std::min(minY, y), maxY = std::max(maxY, y);
|
||||
}
|
||||
if (maxX >= 0)
|
||||
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_);
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
||||
TextureArray::~TextureArray() {
|
||||
if (id_) glDeleteTextures(1, &id_);
|
||||
}
|
||||
|
||||
bool TextureArray::load(const std::vector<std::string>& paths) {
|
||||
std::vector<Image> images(paths.size());
|
||||
for (size_t i = 0; i < paths.size(); ++i) {
|
||||
if (!loadImage(paths[i], images[i])) return false;
|
||||
if (images[i].width != images[0].width || images[i].height != images[0].height) {
|
||||
HD2D_ERR("texture array layer '%s' size mismatch", paths[i].c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
width_ = images[0].width;
|
||||
height_ = images[0].height;
|
||||
layers_ = int(images.size());
|
||||
if (id_) glDeleteTextures(1, &id_);
|
||||
glCreateTextures(GL_TEXTURE_2D_ARRAY, 1, &id_);
|
||||
glTextureStorage3D(id_, mipLevels(width_, height_), GL_SRGB8_ALPHA8, width_, height_, layers_);
|
||||
for (int i = 0; i < layers_; ++i)
|
||||
glTextureSubImage3D(id_, 0, 0, 0, i, width_, height_, 1, GL_RGBA, GL_UNSIGNED_BYTE, images[i].rgba.data());
|
||||
glGenerateTextureMipmap(id_);
|
||||
glTextureParameteri(id_, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
glTextureParameteri(id_, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTextureParameteri(id_, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
||||
glTextureParameteri(id_, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
||||
glTextureParameterf(id_, GL_TEXTURE_MAX_ANISOTROPY, 8.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace hd2d
|
||||
94
engine/src/Water.cpp
Normal file
94
engine/src/Water.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
#include "hd2d/Water.h"
|
||||
|
||||
#include "hd2d/Terrain.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
Water::~Water() {
|
||||
if (vbo_) glDeleteBuffers(1, &vbo_);
|
||||
if (ibo_) glDeleteBuffers(1, &ibo_);
|
||||
if (vao_) glDeleteVertexArrays(1, &vao_);
|
||||
if (shoreTex_) glDeleteTextures(1, &shoreTex_);
|
||||
}
|
||||
|
||||
void Water::buildShoreField(const Terrain& terrain, int texelsPerUnit) {
|
||||
constexpr float kMaxDist = 4.0f;
|
||||
int tw = std::max(1, int(std::ceil((max_.x - min_.x) * float(texelsPerUnit))));
|
||||
int th = std::max(1, int(std::ceil((max_.y - min_.y) * float(texelsPerUnit))));
|
||||
|
||||
// Land tiles (top above the water) that can influence the field.
|
||||
std::vector<glm::vec2> land;
|
||||
int x0 = int(std::floor(min_.x - kMaxDist)), x1 = int(std::ceil(max_.x + kMaxDist));
|
||||
int z0 = int(std::floor(min_.y - kMaxDist)), z1 = int(std::ceil(max_.y + kMaxDist));
|
||||
for (int z = z0; z <= z1; ++z)
|
||||
for (int x = x0; x <= x1; ++x)
|
||||
if (terrain.inBounds(x, z) && float(terrain.level(x, z)) * terrain.levelHeight() > level_)
|
||||
land.emplace_back(float(x), float(z));
|
||||
|
||||
std::vector<float> dist(size_t(tw) * th, kMaxDist);
|
||||
for (int j = 0; j < th; ++j)
|
||||
for (int i = 0; i < tw; ++i) {
|
||||
glm::vec2 p(min_.x + (float(i) + 0.5f) / float(texelsPerUnit), min_.y + (float(j) + 0.5f) / float(texelsPerUnit));
|
||||
float best = kMaxDist;
|
||||
for (const glm::vec2& t : land) { // distance to the tile's unit square
|
||||
glm::vec2 d = glm::max(glm::max(t - p, p - (t + 1.0f)), glm::vec2(0.0f));
|
||||
best = std::min(best, glm::length(d));
|
||||
}
|
||||
dist[size_t(j) * tw + i] = best;
|
||||
}
|
||||
|
||||
if (shoreTex_) glDeleteTextures(1, &shoreTex_);
|
||||
glCreateTextures(GL_TEXTURE_2D, 1, &shoreTex_);
|
||||
glTextureStorage2D(shoreTex_, 1, GL_R16F, tw, th);
|
||||
glTextureSubImage2D(shoreTex_, 0, 0, 0, tw, th, GL_RED, GL_FLOAT, dist.data());
|
||||
glTextureParameteri(shoreTex_, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTextureParameteri(shoreTex_, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTextureParameteri(shoreTex_, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTextureParameteri(shoreTex_, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
|
||||
void Water::build(glm::vec2 minXZ, glm::vec2 maxXZ, float level, float cellSize) {
|
||||
level_ = level;
|
||||
min_ = minXZ;
|
||||
max_ = maxXZ;
|
||||
int nx = std::max(1, int(std::ceil((maxXZ.x - minXZ.x) / cellSize)));
|
||||
int nz = std::max(1, int(std::ceil((maxXZ.y - minXZ.y) / cellSize)));
|
||||
std::vector<glm::vec3> verts;
|
||||
verts.reserve(size_t(nx + 1) * (nz + 1));
|
||||
for (int z = 0; z <= nz; ++z)
|
||||
for (int x = 0; x <= nx; ++x)
|
||||
verts.emplace_back(minXZ.x + (maxXZ.x - minXZ.x) * float(x) / float(nx), level,
|
||||
minXZ.y + (maxXZ.y - minXZ.y) * float(z) / float(nz));
|
||||
std::vector<uint32_t> idx;
|
||||
idx.reserve(size_t(nx) * nz * 6);
|
||||
for (int z = 0; z < nz; ++z)
|
||||
for (int x = 0; x < nx; ++x) {
|
||||
uint32_t a = uint32_t(z * (nx + 1) + x), b = a + 1, c = a + uint32_t(nx + 1), d = c + 1;
|
||||
idx.insert(idx.end(), {a, c, d, a, d, b}); // CCW seen from above
|
||||
}
|
||||
|
||||
if (!vao_) {
|
||||
glCreateVertexArrays(1, &vao_);
|
||||
glCreateBuffers(1, &vbo_);
|
||||
glCreateBuffers(1, &ibo_);
|
||||
}
|
||||
glNamedBufferData(vbo_, GLsizeiptr(verts.size() * sizeof(glm::vec3)), verts.data(), GL_STATIC_DRAW);
|
||||
glNamedBufferData(ibo_, GLsizeiptr(idx.size() * sizeof(uint32_t)), idx.data(), GL_STATIC_DRAW);
|
||||
indexCount_ = GLsizei(idx.size());
|
||||
glVertexArrayVertexBuffer(vao_, 0, vbo_, 0, sizeof(glm::vec3));
|
||||
glVertexArrayElementBuffer(vao_, ibo_);
|
||||
glEnableVertexArrayAttrib(vao_, 0);
|
||||
glVertexArrayAttribFormat(vao_, 0, 3, GL_FLOAT, GL_FALSE, 0);
|
||||
glVertexArrayAttribBinding(vao_, 0, 0);
|
||||
}
|
||||
|
||||
void Water::draw() const {
|
||||
glBindVertexArray(vao_);
|
||||
glDrawElements(GL_TRIANGLES, indexCount_, GL_UNSIGNED_INT, nullptr);
|
||||
}
|
||||
|
||||
} // namespace hd2d
|
||||
135
engine/src/Weather.cpp
Normal file
135
engine/src/Weather.cpp
Normal file
@@ -0,0 +1,135 @@
|
||||
#include "hd2d/Weather.h"
|
||||
|
||||
#include "hd2d/Renderer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
Weather::Weather() {
|
||||
// Heavy overcast rain: weak grey sun, flat cool ambient, thick low fog,
|
||||
// almost no god rays, strong gusty wind.
|
||||
storm.sunIntensity = 0.7f;
|
||||
storm.sunColor = {0.78f, 0.82f, 0.9f};
|
||||
storm.zenith = {0.30f, 0.34f, 0.40f};
|
||||
storm.horizon = {0.42f, 0.45f, 0.50f};
|
||||
storm.ground = {0.13f, 0.15f, 0.13f};
|
||||
storm.ambient = 0.62f;
|
||||
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.windStrength = 0.2f;
|
||||
storm.windGustSpeed = 3.0f;
|
||||
storm.bloomStrength = 0.06f;
|
||||
storm.saturation = 0.82f;
|
||||
}
|
||||
|
||||
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 o;
|
||||
auto m = [t](auto x, auto y) { return x + (y - x) * t; };
|
||||
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.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.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);
|
||||
return o;
|
||||
}
|
||||
|
||||
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_) {
|
||||
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);
|
||||
// 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);
|
||||
|
||||
float k = rain_ * rain_ * (3.0f - 2.0f * rain_); // smoothstep
|
||||
if (rain_ > 0.0f || targetRain_ > 0.0f) apply(mixLook(clear_, storm, k), scene, settings);
|
||||
scene.rain = rain_;
|
||||
scene.wetness = wetness_;
|
||||
|
||||
if (scene.particles) {
|
||||
ParticleSystem& ps = *scene.particles;
|
||||
ps.ambientFade = 1.0f - k;
|
||||
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;
|
||||
|
||||
// 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;
|
||||
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;
|
||||
if (!scene.terrain) continue;
|
||||
float y = scene.terrain->heightAt(p.x, p.y);
|
||||
if (scene.water && y < scene.water->level()) continue;
|
||||
ps.burst({p.x, y + 0.02f, p.y}, 2, color, 0.9f, 0.022f, 0.28f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace hd2d
|
||||
97
engine/src/Window.cpp
Normal file
97
engine/src/Window.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
#include "hd2d/Window.h"
|
||||
|
||||
#include "hd2d/GL.h"
|
||||
#include "hd2d/Log.h"
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
|
||||
namespace hd2d {
|
||||
|
||||
static void APIENTRY glDebugCallback(GLenum, GLenum type, GLuint, GLenum severity, GLsizei,
|
||||
const GLchar* message, const void*) {
|
||||
if (severity == GL_DEBUG_SEVERITY_NOTIFICATION) return;
|
||||
if (type == GL_DEBUG_TYPE_ERROR || severity == GL_DEBUG_SEVERITY_HIGH)
|
||||
HD2D_ERR("GL: %s", message);
|
||||
}
|
||||
|
||||
bool Window::create(int width, int height, const std::string& title, bool vsync, bool resizable) {
|
||||
glfwSetErrorCallback([](int code, const char* msg) { HD2D_ERR("GLFW %d: %s", code, msg); });
|
||||
if (!glfwInit()) return false;
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
|
||||
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
|
||||
glfwWindowHint(GLFW_RESIZABLE, resizable ? GLFW_TRUE : GLFW_FALSE);
|
||||
#ifndef NDEBUG
|
||||
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
|
||||
#endif
|
||||
window_ = glfwCreateWindow(width, height, title.c_str(), nullptr, nullptr);
|
||||
if (!window_) {
|
||||
glfwTerminate();
|
||||
return false;
|
||||
}
|
||||
glfwMakeContextCurrent(window_);
|
||||
glfwSwapInterval(vsync ? 1 : 0);
|
||||
glfwSetWindowUserPointer(window_, this);
|
||||
glfwSetFramebufferSizeCallback(window_, onFramebufferSize);
|
||||
glfwSetScrollCallback(window_, onScroll);
|
||||
glfwGetFramebufferSize(window_, &width_, &height_);
|
||||
|
||||
if (epoxy_has_gl_extension("GL_KHR_debug") || epoxy_gl_version() >= 43) {
|
||||
glEnable(GL_DEBUG_OUTPUT);
|
||||
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
|
||||
glDebugMessageCallback(glDebugCallback, nullptr);
|
||||
}
|
||||
HD2D_LOG("OpenGL %s | %s", glGetString(GL_VERSION), glGetString(GL_RENDERER));
|
||||
|
||||
double mx, my;
|
||||
glfwGetCursorPos(window_, &mx, &my);
|
||||
lastMouse_ = {float(mx), float(my)};
|
||||
return true;
|
||||
}
|
||||
|
||||
void Window::destroy() {
|
||||
if (window_) glfwDestroyWindow(window_);
|
||||
window_ = nullptr;
|
||||
glfwTerminate();
|
||||
}
|
||||
|
||||
bool Window::pollEvents() {
|
||||
resized_ = false;
|
||||
std::copy(std::begin(keys_), std::end(keys_), std::begin(prevKeys_));
|
||||
glfwPollEvents();
|
||||
for (int k = GLFW_KEY_SPACE; k <= GLFW_KEY_LAST && k < 512; ++k)
|
||||
keys_[k] = glfwGetKey(window_, k) == GLFW_PRESS;
|
||||
double mx, my;
|
||||
glfwGetCursorPos(window_, &mx, &my);
|
||||
glm::vec2 m{float(mx), float(my)};
|
||||
mouseDelta_ = m - lastMouse_;
|
||||
lastMouse_ = m;
|
||||
scrollDelta_ = scrollAccum_;
|
||||
scrollAccum_ = 0.0f;
|
||||
return !glfwWindowShouldClose(window_);
|
||||
}
|
||||
|
||||
void Window::swapBuffers() { glfwSwapBuffers(window_); }
|
||||
|
||||
bool Window::keyDown(int key) const { return 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; }
|
||||
void Window::setTitle(const std::string& title) { glfwSetWindowTitle(window_, title.c_str()); }
|
||||
void Window::requestClose() { glfwSetWindowShouldClose(window_, GLFW_TRUE); }
|
||||
|
||||
void Window::onFramebufferSize(GLFWwindow* w, int width, int height) {
|
||||
auto* self = static_cast<Window*>(glfwGetWindowUserPointer(w));
|
||||
self->width_ = width;
|
||||
self->height_ = height;
|
||||
self->resized_ = true;
|
||||
}
|
||||
|
||||
void Window::onScroll(GLFWwindow* w, double, double dy) {
|
||||
static_cast<Window*>(glfwGetWindowUserPointer(w))->scrollAccum_ += float(dy);
|
||||
}
|
||||
|
||||
} // namespace hd2d
|
||||
Reference in New Issue
Block a user