|
/** |
|
* @brief An ECS example with Magnum, part 2 |
|
* |
|
* # What's changed? |
|
* |
|
* - **Global registry** I think it can make sense to have many, but in this case |
|
* there was only one, and this saves us from passing the registry to each system |
|
* - **No mode Mesh::draw** Meshes are now plain-old-data, actual logic being |
|
* implemented by a system. |
|
* |
|
* # What's novel? |
|
* |
|
* - Components are pure types, e.g. Vector3, as opposed to one wrapped in a struct |
|
* Except when otherwise necessary, such as the `ShaderTemplate` component |
|
* - Two-stage initialisation via "Template" and "Instance", to avoid making calls to GPU |
|
* during instantiation of lots of data. Effectively deferring uploads to a bulk operation |
|
* happening during "spawn" (see SpawnSystem) |
|
* - Shaders contain references to things to draw, as opposed to the (default) other way around |
|
* See `ShaderAssignment` component |
|
* |
|
* # Troubles |
|
* |
|
* - I wanted to `using MeshTemplate = MeshTools::MeshData3D` for a component, |
|
* but wasn't able to. Something about `position` being empty once being |
|
* read from within a system. |
|
* - `MeshTools::transformPointsInPlace` isn't working, unsure why. |
|
* Compilation couldn't find its declaration. |
|
* |
|
* # Questions |
|
* |
|
* - How does a camera fit into this? |
|
* That is, do we need a custom Orientation component, e.g. CameraOrientation |
|
* or would it suffice to have a `Camera` "tag" on the entity representing it? |
|
* And how do we make rendering relative a camera? We need some sense of what |
|
* camera is "current" or related to any particular view; in case there are multiple |
|
* such as top, bottom, front and perspective |
|
* - How does shadows fit into this? |
|
* In the case of shadow maps, we need a (set of) draw call(s) to happen prior |
|
* to drawing what is currently drawn. Is it simply a matter of another system, |
|
* and ordering the calls to each system accordingly? Do we need to establish |
|
* some form of relationship between systems, which is dependent on which (and |
|
* for what aspect of it)? |
|
* - How does picking fit into this? |
|
* Like shadows, an ID pass needs to be rendered in parallel with colors/shadows, |
|
* with its own shader (taking only the ID color into account) and independently |
|
* from the rest; i.e. the output isn't relevant outside of calling glReadPixels |
|
* during a picking session. |
|
* - How does *selection* fit into this? |
|
* That is, once something has been picked, I'd like for a highlight of sorts |
|
* to get drawn around the object. Do I add a transient `Selected` component, |
|
* and filter entities by that, in a separate render system? |
|
* |
|
*/ |
|
|
|
#include <Magnum/GL/DefaultFramebuffer.h> |
|
#include <Magnum/GL/Mesh.h> |
|
#include <Magnum/GL/Renderer.h> |
|
#include <Magnum/MeshTools/Compile.h> |
|
#include <Magnum/Platform/Sdl2Application.h> |
|
#include <Magnum/Primitives/Cube.h> |
|
#include <Magnum/Primitives/Icosphere.h> |
|
#include <Magnum/Shaders/Phong.h> |
|
#include <Magnum/Trade/MeshData3D.h> |
|
#include <Magnum/Math/Quaternion.h> |
|
|
|
#include "externals/entt.hpp" |
|
|
|
namespace Magnum { namespace Examples { |
|
|
|
// "There can be only one" - Highlander, 1986 |
|
static entt::registry gRegistry; |
|
|
|
// -------------------------------------------------------------- |
|
// |
|
// Components |
|
// |
|
// -------------------------------------------------------------- |
|
|
|
using Orientation = Quaternion; |
|
using Color = Color3; |
|
|
|
// NOTE: "using" isn't enough here, EnTT requires unique types per component |
|
struct Position : public Vector3 { using Vector3::Vector3; }; |
|
struct Scale : public Vector3 { using Vector3::Vector3; }; |
|
|
|
/** @brief Name, useful for debugging. E.g. to print an entity's name |
|
*/ |
|
using Identity = std::string; |
|
|
|
/** @brief Template for the creation of a mesh |
|
*/ |
|
struct MeshTemplate { |
|
enum { Cube, Sphere, Capsule, Plane } type; |
|
Vector3 extents; |
|
}; |
|
|
|
/** @brief Compiled and uploaded mesh |
|
* |
|
* Including vertex and index buffer, and vertex layout information. |
|
* |
|
*/ |
|
using MeshInstance = GL::Mesh; |
|
|
|
/** @brief Template for the compile and linking of a new shader program |
|
*/ |
|
struct ShaderTemplate { |
|
std::string type; |
|
}; |
|
|
|
/** @brief Compiled and linked shader program |
|
*/ |
|
using ShaderInstance = Shaders::Phong; |
|
|
|
// Connection between drawable entities and a shader entity |
|
using ShaderAssignment = std::vector<entt::registry::entity_type>; |
|
|
|
|
|
// --------------------------------------------------------- |
|
// |
|
// Systems |
|
// |
|
// --------------------------------------------------------- |
|
|
|
/** @brief Affect *all* orientations with the mouse |
|
* |
|
* NOTE: This should probably be more targeted; e.g. affecting only a "camera" |
|
* |
|
*/ |
|
static void MouseMoveSystem(Vector2 distance) { |
|
gRegistry.view<Orientation>().each([distance](auto& ori) { |
|
ori = ( |
|
Quaternion::rotation(Rad{ distance.y() }, Vector3(1.0f, 0, 0)) * |
|
ori * |
|
Quaternion::rotation(Rad{ distance.x() }, Vector3(0, 1.0f, 0)) |
|
).normalized(); |
|
}); |
|
} |
|
|
|
/** |
|
* @brief Shift all colors on mouse release |
|
* |
|
* NOTE: Like the above, this should probably be more targeted; using ECS "tags"? |
|
* |
|
*/ |
|
static void MouseReleaseSystem() { |
|
gRegistry.view<Color>().each([](auto& color) { |
|
color = Color3::fromHsv({ color.hue() + 50.0_degf, 1.0f, 1.0f }); |
|
}); |
|
} |
|
|
|
static void AnimationSystem() { |
|
Debug() << "Animating.."; |
|
} |
|
|
|
static void PhysicsSystem() { |
|
Debug() << "Simulating.."; |
|
} |
|
|
|
/** |
|
* @brief Upload new data to the GPU |
|
* |
|
* Whenever a new item spawns, it'll carry data pending an upload |
|
* to the GPU. A spawned component may be replaced by assigning |
|
* a new template to an entity. |
|
* |
|
*/ |
|
static void SpawnSystem() { |
|
gRegistry.view<Identity, ShaderTemplate>().each([](auto entity, auto& id, auto& tmpl) { |
|
Debug() << "Instantiating shader from template for:" << id; |
|
|
|
gRegistry.remove<ShaderTemplate>(entity); |
|
gRegistry.assign_or_replace<ShaderInstance>( |
|
entity, |
|
|
|
// Only one option, for now |
|
tmpl.type == "phong" ? Shaders::Phong{} |
|
: Shaders::Phong{} |
|
); |
|
}); |
|
|
|
gRegistry.view<Identity, MeshTemplate>().each([](auto entity, auto& id, auto& tmpl) { |
|
Debug() << "Instantiating mesh from template for:" << id; |
|
|
|
auto data = tmpl.type == MeshTemplate::Cube ? Primitives::cubeSolid() : |
|
MeshTemplate::Sphere ? Primitives::icosphereSolid(3) : |
|
Primitives::icosphereSolid(3); |
|
|
|
// NOTE: The below isn't working |
|
// NOTE: Cannot find `transformPointsInPlace` |
|
|
|
// Matrix4 transformation = Matrix4::scaling(tmpl.extents); |
|
//MeshTools::transformPointsInPlace(transformation, data.positions(0)); |
|
//MeshTools::transformVectorsInPlace(transformation, data.normals(0)); |
|
|
|
gRegistry.remove<MeshTemplate>(entity); |
|
gRegistry.assign_or_replace<MeshInstance>(entity, MeshTools::compile(data)); |
|
}); |
|
} |
|
|
|
/** |
|
* @brief Facilitate new templates being added for either shaders or meshes |
|
* |
|
*/ |
|
static void ChangeSystem() {} |
|
|
|
/** |
|
* @brief Destroy entities with a `Destroyed` component |
|
* |
|
*/ |
|
static void CleanupSystem() {} |
|
|
|
/** |
|
* @brief Produce pixels by calling on the uploaded shader |
|
* |
|
* Meshes are drawn per-shader. That is, a shader is associated to multiple renderables |
|
* |
|
*/ |
|
static void RenderSystem(Vector2i viewport, Matrix4 projection) { |
|
GL::defaultFramebuffer.clear(GL::FramebufferClear::Color | GL::FramebufferClear::Depth); |
|
GL::defaultFramebuffer.setViewport({{}, viewport}); |
|
|
|
GL::Renderer::enable(GL::Renderer::Feature::DepthTest); |
|
|
|
Debug() << "Drawing.."; |
|
|
|
gRegistry.view<Identity, ShaderAssignment, ShaderInstance>().each([projection](auto& id, auto& assignment, auto& shader) { |
|
|
|
Debug() << " ..using shader:" << id; |
|
|
|
// NOTE: Doing double-duty; calls to `shader.set*` also call on `glUseProgram` |
|
// ..except it shouldn't have to. |
|
glUseProgram(shader.id()); |
|
|
|
// Uniforms applicable to *all* assigned meshes |
|
shader |
|
.setLightPosition({7.0f, 7.0f, 2.5f}) |
|
.setLightColor(Color3{1.0f}) |
|
.setProjectionMatrix(projection); |
|
|
|
for (auto& entity : assignment) { |
|
// NOTE: Looping through entities from within a loop of components |
|
// Is this a good idea? What is the alternative? |
|
|
|
const auto& [id, pos, ori, scale, color, mesh] = gRegistry.get< |
|
Identity, Position, Orientation, Scale, Color, MeshInstance |
|
>(entity); |
|
|
|
Debug() << " - " << id; |
|
|
|
auto transform = ( |
|
Matrix4::scaling(scale) * |
|
Matrix4::rotation(ori.angle(), ori.axis().normalized()) * |
|
Matrix4::translation(pos) |
|
); |
|
|
|
shader |
|
.setDiffuseColor(color) |
|
.setAmbientColor(Color3::fromHsv({color.hue(), 1.0f, 0.3f})) |
|
.setTransformationMatrix(transform) |
|
.setNormalMatrix(transform.rotationScaling()); |
|
|
|
// NOTE: Assumes indexed draw, which is fine for this example |
|
glBindVertexArray(mesh.id()); |
|
glDrawElements(GLenum(mesh.primitive()), |
|
mesh.count(), |
|
GLenum(mesh.indexType()), |
|
reinterpret_cast<GLvoid*>(nullptr)); |
|
glBindVertexArray(0); |
|
} |
|
}); |
|
} |
|
|
|
// --------------------------------------------------------- |
|
// |
|
// Application |
|
// |
|
// --------------------------------------------------------- |
|
|
|
using namespace Magnum::Math::Literals; |
|
|
|
class ECSExample : public Platform::Application { |
|
public: |
|
explicit ECSExample(const Arguments& arguments); |
|
~ECSExample(); |
|
|
|
private: |
|
void drawEvent() override; |
|
void mousePressEvent(MouseEvent& event) override; |
|
void mouseReleaseEvent(MouseEvent& event) override; |
|
void mouseMoveEvent(MouseMoveEvent& event) override; |
|
|
|
Matrix4 _projection; |
|
Vector2i _previousMousePosition; |
|
}; |
|
|
|
ECSExample::~ECSExample() { |
|
|
|
// Let go of all references to components |
|
// |
|
// If we don't do this, gRegistry is destroyed *after* the application, |
|
// which means after the OpenGL context has been freed, resulting in shaders |
|
// being unable to clean themselves up. |
|
gRegistry.reset(); |
|
} |
|
|
|
ECSExample::ECSExample(const Arguments& arguments) : |
|
Platform::Application{ arguments, Configuration{} |
|
.setTitle("Magnum ECS Example") } |
|
{ |
|
GL::Renderer::enable(GL::Renderer::Feature::DepthTest); |
|
GL::Renderer::enable(GL::Renderer::Feature::FaceCulling); |
|
|
|
_projection = |
|
Matrix4::perspectiveProjection( |
|
35.0_degf, Vector2{ windowSize() }.aspectRatio(), 0.01f, 100.0f) * |
|
Matrix4::translation(Vector3::zAxis(-10.0f)); |
|
|
|
auto box = gRegistry.create(); |
|
auto sphere = gRegistry.create(); |
|
auto phong = gRegistry.create(); |
|
|
|
// Box |
|
gRegistry.assign<Identity>(box, "box"); |
|
gRegistry.assign<Position>(box, Vector3{}); |
|
gRegistry.assign<Orientation>(box, Quaternion::rotation(5.0_degf, Vector3(0, 1.0f, 0))); |
|
gRegistry.assign<Scale>(box, Vector3{1.0f, 1.0f, 1.0f}); |
|
gRegistry.assign<Color>(box, Color3{ 0.1f, 0.6f, 0.8f }); |
|
gRegistry.assign<MeshTemplate>(box, MeshTemplate::Cube, Vector3(2.0f, 0.5f, 2.0f)); |
|
|
|
// Sphere |
|
gRegistry.assign<Identity>(sphere, "sphere"); |
|
gRegistry.assign<Position>(sphere, Vector3{}); |
|
gRegistry.assign<Orientation>(sphere, Quaternion::rotation(5.0_degf, Vector3(0, 1.0f, 0))); |
|
gRegistry.assign<Scale>(sphere, Vector3{1.2f, 1.2f, 1.2f}); |
|
gRegistry.assign<Color>(sphere, Color3{ 0.9f, 0.6f, 0.2f }); |
|
gRegistry.assign<MeshTemplate>(sphere, MeshTemplate::Sphere, Vector3(1.0f, 1.0f, 1.0f)); |
|
|
|
// Phong |
|
gRegistry.assign<Identity>(phong, "phong"); |
|
gRegistry.assign<ShaderTemplate>(phong); |
|
|
|
// Connect vertex buffers to shader program |
|
// Called on changes to assignment, e.g. a new torus is assigned this shader |
|
gRegistry.assign<ShaderAssignment>(phong, std::vector<entt::registry::entity_type>{box, sphere}); |
|
} |
|
|
|
void ECSExample::drawEvent() { |
|
GL::defaultFramebuffer.clear( |
|
GL::FramebufferClear::Color | GL::FramebufferClear::Depth |
|
); |
|
|
|
auto viewport = GL::defaultFramebuffer.viewport().size(); |
|
|
|
SpawnSystem(); |
|
AnimationSystem(); |
|
PhysicsSystem(); |
|
RenderSystem(viewport, _projection); |
|
CleanupSystem(); |
|
|
|
swapBuffers(); |
|
} |
|
|
|
void ECSExample::mousePressEvent(MouseEvent& event) { |
|
if (event.button() != MouseEvent::Button::Left) return; |
|
_previousMousePosition = event.position(); |
|
event.setAccepted(); |
|
} |
|
|
|
void ECSExample::mouseReleaseEvent(MouseEvent& event) { |
|
if (event.button() != MouseEvent::Button::Left) return; |
|
|
|
// Should the system handle all mouse events, instead of individual ones? |
|
MouseReleaseSystem(); |
|
|
|
event.setAccepted(); |
|
redraw(); |
|
} |
|
|
|
void ECSExample::mouseMoveEvent(MouseMoveEvent& event) { |
|
if (!(event.buttons() & MouseMoveEvent::Button::Left)) return; |
|
|
|
const float sensitivity = 3.0f; |
|
const Vector2 distance = ( |
|
Vector2{ event.position() - _previousMousePosition } / |
|
Vector2{ GL::defaultFramebuffer.viewport().size() } |
|
) * sensitivity; |
|
|
|
// Should the system compute delta? |
|
// If so, where does state go, i.e. _previousMousePosition? |
|
MouseMoveSystem(distance); |
|
|
|
_previousMousePosition = event.position(); |
|
event.setAccepted(); |
|
|
|
redraw(); |
|
} |
|
|
|
}} |
|
|
|
MAGNUM_APPLICATION_MAIN(Magnum::Examples::ECSExample) |
Thanks @skypjack; though I'm still not a fan of having to make that last-minute decision; seems like memory fragmentation and a more complicated function body, when the outer loop is so clean. Would you venture this route?
What I was looking for was something like this:
gRegistry.view<Identity, Position, Orientation, ...>(assignment).each([](...) {}
Where the components are limited to only the entities in the
assignment
vector; as thoughassignment
was a "sub-registry" of sorts. Maybe it would make sense to haveassignment
be a dynamically generated registry, instead of astd::vector<entity_type>
?