|
/** |
|
* @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 guys!
Yes. :( I may be using an older version, 2019.01 from VcPkg.
Cool, good to know.
I get that distributing C++ libraries is tricky in general; coming from a Python background where any library can be made accessible as
pip install my_library
, anything involving compilers and cloning git repos is a little overwhelming. I mentioned this somewhere before, but so far the simplest mechanism I've encountered (in my ~2 months of C++, so grains of salt and all that) has been bs::framework, and their downloadable zip file per platform. EvenVcPkg
didn't cut it; because it doesn't distribute binaries. It builds it "live" which still leaves a lot of requirements on part of the user. And I really didn't like its "magic" hooks into things like MSVC; where it somehow magically finds includes and libraries without me specifying them. Makes it tricky to try and understand what's going on.It seems to me that whatever platform you are on, a new user is looking for:
In an ideal world, I could download a folder with an
include/
andlib/
in it and call it a day. And worry about optimising that later, once I'm hooked and committed.I'd rather not though.
Ah.. yes that does makes sense.. I hadn't considered other platforms.
Ah, yes sorry I meant in this particular loop, that call was superflous, not in Magnum in general.
That does sound promising, will investigate this one.
Good point! But that's what's happening here as well I think, with the MeshTemplate (and ShaderTemplate) being discarded once converted to their corresponding instances. I haven't double-checked that they actually get cleaned up, whether there's some reference counting going on to do that automatically once it doesn't have an owner.