|
/** |
|
* @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) |
Eek, I would be happy to, but is my naive (I'm very new at this) example suited for others to learn from? :S
I wouldn't mind re-implementing the ECSY example however, maybe we could include that? Looks like a straightforward yet valuable enough example and experience.
In addition, I also just found this!
This could make for both great examples for EnTT, but also an API, implementation and performance comparison with this other framework. For one, I'm very happy about the API you've got with EnTT, compared with FLECS, which reads like macro and poorly named acronyms
ecs_fini()
(?) and mixture of snake_case, CamelCase and UPPERCASE mess. However I'm quite impressed by the dashboard that I have no idea how it works. :O It also looks like it's not just an ECS framework, but more like a game engine, with a game loop and "canvas" with drawables. And they've also taken a crack at explaining ecs which might also be helpful for your own documentation.Also I don't know about you, but some of the best projects I know have references to "Similar Projects" somewhere in their documentation; it shows the world that you are aware of their existence and have incorporated the best of their ideas. In the ideal case there's a comparison as well, but a mention is a good start.