324 lines
14 KiB
C++
324 lines
14 KiB
C++
// Meshes and the components the renderer understands. Nothing here depends on raylib/OpenGL,
|
|
// so it is usable (and testable) headless. Create meshes through Resources (resources.hpp) so
|
|
// identical meshes are shared instead of duplicated.
|
|
#pragma once
|
|
#include "ecs.hpp"
|
|
#include "material.hpp"
|
|
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace eng {
|
|
|
|
// Unindexed triangle list: vertices i*3 .. i*3+2 form triangle i, counter-clockwise = front face.
|
|
struct MeshData {
|
|
std::vector<Vec3> positions;
|
|
std::vector<Vec3> normals;
|
|
std::vector<Rgba> colors;
|
|
// Optional: one per vertex, or none at all (empty). (0, 0) is the top-left corner of a texture, v grows downwards.
|
|
// Without them a material maps its textures from the world instead (UvMode::Auto, see material.hpp).
|
|
std::vector<Vec2> texcoords;
|
|
|
|
[[nodiscard]] std::size_t vertexCount() const { return positions.size(); }
|
|
[[nodiscard]] std::size_t triangleCount() const { return positions.size() / 3; }
|
|
[[nodiscard]] bool empty() const { return positions.empty(); }
|
|
[[nodiscard]] bool hasTexcoords() const { return !positions.empty() && texcoords.size() == positions.size(); }
|
|
|
|
void addTriangle(Vec3 a, Vec3 b, Vec3 c, Vec3 na, Vec3 nb, Vec3 nc, Rgba ca, Rgba cb, Rgba cc) {
|
|
positions.insert(positions.end(), {a, b, c});
|
|
normals.insert(normals.end(), {na, nb, nc});
|
|
colors.insert(colors.end(), {ca, cb, cc});
|
|
if (!texcoords.empty()) texcoords.resize(positions.size()); // a mesh that has texture coordinates keeps one per vertex
|
|
}
|
|
void addTriangle(Vec3 a, Vec3 b, Vec3 c, Vec3 na, Vec3 nb, Vec3 nc, Rgba ca, Rgba cb, Rgba cc, Vec2 ta, Vec2 tb, Vec2 tc) {
|
|
texcoords.resize(positions.size()); // triangles added without them so far get (0, 0)
|
|
positions.insert(positions.end(), {a, b, c});
|
|
normals.insert(normals.end(), {na, nb, nc});
|
|
colors.insert(colors.end(), {ca, cb, cc});
|
|
texcoords.insert(texcoords.end(), {ta, tb, tc});
|
|
}
|
|
void append(const MeshData& o) {
|
|
const std::size_t had = positions.size();
|
|
positions.insert(positions.end(), o.positions.begin(), o.positions.end());
|
|
normals.insert(normals.end(), o.normals.begin(), o.normals.end());
|
|
colors.insert(colors.end(), o.colors.begin(), o.colors.end());
|
|
if (!texcoords.empty() || !o.texcoords.empty()) { // whichever side has them: the other one's vertices get (0, 0)
|
|
texcoords.resize(had);
|
|
texcoords.insert(texcoords.end(), o.texcoords.begin(), o.texcoords.end());
|
|
texcoords.resize(positions.size());
|
|
}
|
|
}
|
|
[[nodiscard]] Aabb bounds() const {
|
|
Aabb b;
|
|
for (const Vec3& p : positions) b.expand(p);
|
|
return b;
|
|
}
|
|
// Signed volume of a closed, outward-facing mesh (divergence theorem).
|
|
[[nodiscard]] double signedVolume() const {
|
|
double v = 0;
|
|
for (std::size_t i = 0; i + 2 < positions.size(); i += 3)
|
|
v += static_cast<double>(dot(positions[i], cross(positions[i + 1], positions[i + 2]))) / 6.0;
|
|
return v;
|
|
}
|
|
};
|
|
|
|
// Immutable, shareable mesh. The renderer keeps one GPU copy per Mesh no matter how many
|
|
// entities use it (and draws them with GPU instancing); physics shares collision shapes too.
|
|
class Mesh;
|
|
using MeshPtr = std::shared_ptr<const Mesh>;
|
|
|
|
class Mesh {
|
|
public:
|
|
explicit Mesh(MeshData data);
|
|
static MeshPtr create(MeshData data); // a unique mesh (not cached)
|
|
|
|
[[nodiscard]] const MeshData& data() const noexcept { return data_; }
|
|
[[nodiscard]] const Aabb& bounds() const noexcept { return bounds_; }
|
|
[[nodiscard]] std::uint64_t id() const noexcept { return id_; } // unique for the process lifetime
|
|
|
|
private:
|
|
MeshData data_;
|
|
Aabb bounds_;
|
|
std::uint64_t id_;
|
|
};
|
|
|
|
// ---- components ---------------------------------------------------------------------------
|
|
|
|
// Base of every component that draws something (or lights the scene).
|
|
//
|
|
// Hiding is purely visual: scripts keep updating, physics keeps simulating, colliders keep colliding
|
|
// (use Entity::setEntityEnabled() to switch an entity off completely). A hidden mesh also casts no
|
|
// shadow and a hidden light is off.
|
|
//
|
|
// visible this component draws nothing while false
|
|
// childrenVisible false hides every entity BELOW this one as well; this component itself is
|
|
// controlled by `visible`, so a parent can stay visible while its subtree is hidden
|
|
//
|
|
// Entity::setVisible(false) is the one-call version (hides the entity and everything below it); it works
|
|
// on plain group entities too, by giving them a Visibility component.
|
|
class Renderable : public Component {
|
|
public:
|
|
bool visible = true;
|
|
bool childrenVisible = true;
|
|
|
|
// Hides/shows this component and, by default, everything below its entity.
|
|
void setVisible(bool show, bool includeChildren = true) {
|
|
visible = show;
|
|
if (includeChildren) childrenVisible = show;
|
|
}
|
|
// `visible` and no ancestor hides its children.
|
|
[[nodiscard]] bool visibleInHierarchy() const;
|
|
};
|
|
|
|
// Draws nothing: gives a plain group entity the same visibility switches as a renderer, so it can hide
|
|
// its subtree (Entity::setVisible adds one automatically).
|
|
class Visibility : public Renderable {};
|
|
|
|
class MeshRenderer : public Renderable {
|
|
public:
|
|
MeshPtr mesh;
|
|
Rgba tint{255, 255, 255, 255}; // multiplied with the vertex colours (and with a material's albedo)
|
|
|
|
// Surface. The defaults give a matte, non-glowing material.
|
|
float roughness = 0.85f; // 0 = mirror-like highlights .. 1 = fully diffuse
|
|
float metallic = 0.0f; // 0 = plastic/stone/wood .. 1 = metal (the colour tints the reflection)
|
|
float emission = 0.0f; // self-lit: adds colour * emission regardless of lights (glowing: 1..3)
|
|
// A material (material.hpp) with textures and more properties. While it is set, it decides the surface: the three
|
|
// numbers above are not used (the tint still is). Share one material between as many renderers as you like.
|
|
MaterialPtr material;
|
|
bool castShadows = true;
|
|
bool receiveShadows = true;
|
|
bool unlit = false; // ignore all lighting and draw the plain colour
|
|
};
|
|
|
|
// run() renders with the first enabled Camera. It looks down the entity's -Z.
|
|
class Camera : public Component {
|
|
public:
|
|
float fovDeg = 60.0f; // vertical field of view (perspective)
|
|
float nearPlane = 0.1f;
|
|
float farPlane = 1000.0f;
|
|
bool orthographic = false;
|
|
float orthoSize = 10.0f; // vertical extent in world units when orthographic
|
|
};
|
|
|
|
enum class LightType : std::uint8_t {
|
|
Directional, // the sun: parallel rays along the entity's -Z, position ignored
|
|
Point, // radiates from the entity's position
|
|
Spot, // cone from the entity's position along its -Z
|
|
};
|
|
|
|
// A light source. Aim directional/spot lights with Transform::lookAt / rotateTo. A world without any
|
|
// Light gets a default sun. Every light is a Renderable: a hidden light is off.
|
|
//
|
|
// Brightness: for the sun, `intensity` 1 lights a surface facing it with its plain colour. Point and spot
|
|
// lights fall off with the inverse square of the distance (smoothly reaching zero at `range`), so give
|
|
// them more: `intensity` is the brightness one world unit away.
|
|
class Light : public Renderable {
|
|
public:
|
|
LightType type = LightType::Directional;
|
|
Vec3 color{1.0f, 0.98f, 0.92f};
|
|
float intensity = 1.0f;
|
|
|
|
float range = 20.0f; // Point/Spot: distance at which the light has faded to nothing
|
|
float spotAngleDeg = 50.0f; // Spot: full cone angle
|
|
float spotSoftness = 0.25f; // Spot: 0 = hard edge .. 1 = fades from the axis outwards
|
|
|
|
bool castShadows = true; // Directional and Spot lights (see Environment::maxShadowLights)
|
|
float shadowBias = 1.0f; // multiplier on the automatic depth bias: raise for acne, lower for floating shadows
|
|
|
|
static Light directional(Vec3 color = {1.0f, 0.98f, 0.92f}, float intensity = 1.0f) {
|
|
Light l;
|
|
l.color = color;
|
|
l.intensity = intensity;
|
|
return l;
|
|
}
|
|
static Light point(Vec3 color, float intensity = 30.0f, float range = 15.0f) {
|
|
Light l;
|
|
l.type = LightType::Point;
|
|
l.color = color;
|
|
l.intensity = intensity;
|
|
l.range = range;
|
|
l.castShadows = false;
|
|
return l;
|
|
}
|
|
static Light spot(Vec3 color, float intensity = 60.0f, float range = 25.0f, float angleDeg = 50.0f) {
|
|
Light l;
|
|
l.type = LightType::Spot;
|
|
l.color = color;
|
|
l.intensity = intensity;
|
|
l.range = range;
|
|
l.spotAngleDeg = angleDeg;
|
|
return l;
|
|
}
|
|
};
|
|
|
|
enum class ToneMapping : std::uint8_t { None, Reinhard, Aces };
|
|
|
|
// Scene-wide look: ambient light, exposure, fog and shadow quality. The first enabled Environment
|
|
// in the world is used; without one the defaults below apply.
|
|
class Environment : public Component {
|
|
public:
|
|
// Hemisphere ambient: `ambientSky` lights surfaces facing up, `ambientGround` surfaces facing down.
|
|
// AmbientZone components override this inside their region.
|
|
Vec3 ambientSky{0.42f, 0.47f, 0.56f};
|
|
Vec3 ambientGround{0.20f, 0.18f, 0.16f};
|
|
float ambientIntensity = 1.0f;
|
|
|
|
float exposure = 1.0f;
|
|
ToneMapping toneMapping = ToneMapping::Aces;
|
|
|
|
// Distance fog (exponential squared); 0 = off.
|
|
Vec3 fogColor{0.62f, 0.70f, 0.82f};
|
|
float fogDensity = 0.0f;
|
|
|
|
// Shadows
|
|
bool shadows = true;
|
|
int shadowResolution = 2048; // texels per side, rounded to a power of two in [256, 4096]
|
|
float shadowDistance = 60.0f; // how far from the camera the sun's shadow reaches
|
|
float shadowSoftness = 1.0f; // filter radius in texels (0 = hard edges)
|
|
float shadowStrength = 1.0f; // 0 = no visible shadow .. 1 = fully dark
|
|
int maxShadowLights = 3; // shadow maps per frame: the sun first, then the nearest shadowing spots
|
|
|
|
// Ambient zones are baked into a 3D grid this fine (world units per cell), coarsened automatically
|
|
// when the zones span more than `zoneMaxCells` cells.
|
|
float zoneCellSize = 0.5f;
|
|
int zoneMaxCells = 262144;
|
|
};
|
|
|
|
enum class ZoneShape : std::uint8_t {
|
|
Box, // `size`, centred on the entity
|
|
Sphere, // `radius`, centred on the entity (an ellipsoid when the entity is scaled)
|
|
Capsule, // `radius` and `height` (end to end) along local Y, centred
|
|
Cylinder, // `radius` and `height` along local Y, centred
|
|
Polygon, // `polygon` on the local XZ plane, extruded from y = 0 up to `height`
|
|
Mesh, // any closed mesh in local space (e.g. an extruded SVG shape)
|
|
};
|
|
enum class ZoneMode : std::uint8_t {
|
|
Replace, // the zone's ambient takes the place of what is below it
|
|
Add, // adds its ambient to what is below it
|
|
Multiply, // tints/darkens what is below it (1 = unchanged)
|
|
};
|
|
|
|
// A region of space with its own ambient light.
|
|
//
|
|
// The shape lives in the entity's local space, so moving, rotating or scaling the entity moves, turns
|
|
// and resizes the zone. It can be a box, sphere, capsule, cylinder, any polygon or any closed mesh, and
|
|
// fades in over `falloff` world units outside its boundary (0 = hard edge). Overlapping zones are applied
|
|
// in `priority` order (the highest ends up on top); where no zone applies the Environment's ambient is used.
|
|
// Zones are baked into a grid (see Environment::zoneCellSize); the light is evaluated per pixel, so large
|
|
// meshes crossing a boundary shade correctly. Disable the component to switch a zone off. Moving, resizing or
|
|
// recolouring a zone re-bakes only what it touches (about a millisecond on a level of ~90,000 cells), so zones
|
|
// that follow a player or breathe are fine; a bigger grid (smaller zoneCellSize, larger world) costs more.
|
|
class AmbientZone : public Component {
|
|
public:
|
|
ZoneShape shape = ZoneShape::Box;
|
|
Vec3 size{4.0f, 3.0f, 4.0f};
|
|
float radius = 2.0f;
|
|
float height = 4.0f;
|
|
std::vector<Vec2> polygon;
|
|
MeshPtr mesh;
|
|
|
|
Vec3 sky{0.42f, 0.47f, 0.56f}; // ambient from above inside the zone
|
|
Vec3 ground{0.20f, 0.18f, 0.16f}; // ambient from below inside the zone
|
|
float intensity = 1.0f;
|
|
ZoneMode mode = ZoneMode::Replace;
|
|
float falloff = 1.0f; // world units over which the zone fades in outside its boundary
|
|
int priority = 0;
|
|
|
|
static AmbientZone box(Vec3 size, Vec3 sky, Vec3 ground) {
|
|
AmbientZone z;
|
|
z.shape = ZoneShape::Box;
|
|
z.size = size;
|
|
z.sky = sky;
|
|
z.ground = ground;
|
|
return z;
|
|
}
|
|
static AmbientZone sphere(float radius, Vec3 sky, Vec3 ground) {
|
|
AmbientZone z;
|
|
z.shape = ZoneShape::Sphere;
|
|
z.radius = radius;
|
|
z.sky = sky;
|
|
z.ground = ground;
|
|
return z;
|
|
}
|
|
static AmbientZone polygonal(std::vector<Vec2> outline, float height, Vec3 sky, Vec3 ground) {
|
|
AmbientZone z;
|
|
z.shape = ZoneShape::Polygon;
|
|
z.polygon = std::move(outline);
|
|
z.height = height;
|
|
z.sky = sky;
|
|
z.ground = ground;
|
|
return z;
|
|
}
|
|
static AmbientZone fromMesh(MeshPtr closedMesh, Vec3 sky, Vec3 ground) {
|
|
AmbientZone z;
|
|
z.shape = ZoneShape::Mesh;
|
|
z.mesh = std::move(closedMesh);
|
|
z.sky = sky;
|
|
z.ground = ground;
|
|
return z;
|
|
}
|
|
};
|
|
|
|
// Screen-space text drawn on top of the 3D scene (HUDs, debug read-outs).
|
|
class Text : public Renderable {
|
|
public:
|
|
std::string value;
|
|
Vec2 position{10, 10}; // pixels from the top-left corner
|
|
float size = 20; // pixels
|
|
Rgba color{20, 20, 30, 255};
|
|
};
|
|
|
|
// Calls f(Renderable&) for every renderer-type component on `e` (the renderer's visibility rules read
|
|
// these). Add new Renderable component types here.
|
|
template <class F> void forEachRenderable(Entity e, F&& f) {
|
|
if (MeshRenderer* c = e.tryGet<MeshRenderer>()) f(static_cast<Renderable&>(*c));
|
|
if (Light* c = e.tryGet<Light>()) f(static_cast<Renderable&>(*c));
|
|
if (Text* c = e.tryGet<Text>()) f(static_cast<Renderable&>(*c));
|
|
if (Visibility* c = e.tryGet<Visibility>()) f(static_cast<Renderable&>(*c));
|
|
}
|
|
|
|
} // namespace eng
|