|
/** |
|
* @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) |
(the usual apology for taking centuries to reply -- sorry)
transformPointsInPlace()
is there for quite a few years already, you shouldn't have any issues with that. Ping me on Gitter if you're still struggling with this part.That's planned for Windows (macOS and Linux fortunately have easy-to-use packaging systems already), but so far I didn't find time to try setting up a CI to produce nightly and release builds. Might try the new GitHub Actions for that. I believed strongly in vcpkg at first, but damn it jut does way too much and is soo brittle.
These days I'm working on getting the CMake subproject setup more convenient to use, while it makes you tied to CMake, this could be the single reliable cross-platform way to do things (even though basically requiring you to build everything). Last blocker is SDL, I'll see how easy/hard it is to use as a CMake subproject, maybe switching to GlfwApplication instead if that will make the setup simpler.