Implement ECS with EnTT and a submission based renderer
This commit is contained in:
parent
08bcb56a5f
commit
3b51dce796
|
@ -0,0 +1,10 @@
|
|||
# Get the entt header libraries.
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
entt
|
||||
GIT_REPOSITORY https://github.com/skypjack/entt.git
|
||||
GIT_TAG v3.10.3
|
||||
)
|
||||
FetchContent_MakeAvailable(entt)
|
||||
message("Downloaded entt library to: ${entt_SOURCE_DIR}")
|
||||
set(ENTT_INCLUDE_DIR ${entt_SOURCE_DIR}/src)
|
|
@ -4,6 +4,7 @@ project(SimpleEngine)
|
|||
|
||||
include(cmrc/CMakeRC.cmake)
|
||||
include(CMake/GetStbLibraries.cmake)
|
||||
include(CMake/GetEnTT.cmake)
|
||||
|
||||
# Add some CMake options:
|
||||
option(SIMPLE_ENGINE_BUILD_EXAMPLES "Build example projects" ON)
|
||||
|
@ -47,11 +48,11 @@ target_link_libraries(simpleengine PUBLIC ${OPENGL_LIBRARIES})
|
|||
target_link_libraries(simpleengine PUBLIC assimp)
|
||||
target_link_libraries(simpleengine PRIVATE simpleengine_resources)
|
||||
|
||||
target_include_directories(simpleengine PUBLIC ${STB_INCLUDE_DIR})
|
||||
|
||||
# Include some dependencies' include directories
|
||||
target_include_directories(simpleengine PUBLIC ${OPENGL_INCLUDE_DIR})
|
||||
target_include_directories(simpleengine PUBLIC ${GLM_INCLUDE_DIRS})
|
||||
target_include_directories(simpleengine PUBLIC ${STB_INCLUDE_DIR})
|
||||
target_include_directories(simpleengine PUBLIC ${ENTT_INCLUDE_DIR})
|
||||
|
||||
# Add examples as a target if the user has them enabled
|
||||
if (SIMPLE_ENGINE_BUILD_EXAMPLES)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
#include "entt/entity/fwd.hpp"
|
||||
#include "simpleengine/camera.h"
|
||||
#include "simpleengine/ecs/component/mesh_component.h"
|
||||
#include <simpleengine/ecs/component/model_component.h>
|
||||
#include "simpleengine/ecs/component/transform_component.h"
|
||||
#include "simpleengine/ecs/entity.h"
|
||||
#include "simpleengine/entity_manager.h"
|
||||
#include "simpleengine/gfx/light.h"
|
||||
#include "simpleengine/gfx/material.h"
|
||||
#include "simpleengine/gfx/mesh.h"
|
||||
|
@ -20,6 +21,8 @@
|
|||
#include <simpleengine/gfx/shaders/core_3d_shader.h>
|
||||
#include <simpleengine/gfx/model.h>
|
||||
|
||||
#include <simpleengine/scene.h>
|
||||
|
||||
//#include <simpleengine/scene.h>
|
||||
|
||||
#include <glm/ext/matrix_clip_space.hpp>
|
||||
|
@ -170,25 +173,29 @@ int main(int argc, char *argv[]) {
|
|||
textures.emplace(white_texture.type, std::vector<se::gfx::Texture>{ white_texture });
|
||||
se::gfx::Material white_material(textures, 1.f, 0.f, 0.f, 0.f, 0.f);
|
||||
|
||||
// Create a renderer
|
||||
auto renderer = std::make_shared<se::gfx::Renderer>(game.get_window(), core_shader);
|
||||
game.add_renderable(renderer);
|
||||
|
||||
// Create a Scene and give it the renderer
|
||||
auto scene = std::make_shared<se::Scene>(renderer);
|
||||
game.add_event(scene);
|
||||
|
||||
// Create an Entity in the Scene and add components to it.
|
||||
se::ecs::Entity entity = scene->create_entity();
|
||||
entity.add_component<se::ModelComponent>("examples/dev_testing/resources/dragon.obj");
|
||||
auto& transform_comp = entity.add_component<se::TransformComponent>();
|
||||
transform_comp.translate(12.f, -4.f, 0.f);
|
||||
|
||||
|
||||
// Create the entity and add the model component to it.
|
||||
/* auto entity = std::make_shared<simpleengine::Entity>();
|
||||
entity->add_component<se::MeshComponent>(cube_vertices, cube_indicies, white_material, true);
|
||||
entity->translate(3.5f, 0.f, 0.f); */
|
||||
|
||||
auto entity = std::make_shared<simpleengine::Entity>();
|
||||
/* auto entity = std::make_shared<simpleengine::Entity>();
|
||||
entity->add_component<se::ModelComponent>("examples/dev_testing/resources/dragon.obj");
|
||||
entity->translate(12.f, -4.f, 0.f);
|
||||
|
||||
// Create a renderer and submit the entity into it.
|
||||
auto renderer = std::make_shared<se::gfx::Renderer>(game.get_window(), core_shader);
|
||||
renderer->enable_debug();
|
||||
renderer->submit_entity(entity);
|
||||
game.add_renderable(renderer);
|
||||
|
||||
// Create an EntityManager, and submit the entity into it.
|
||||
auto ecs_manager = std::make_shared<se::EntityManager>();
|
||||
ecs_manager->submit_entity(entity);
|
||||
game.add_event(ecs_manager);
|
||||
entity->translate(12.f, -4.f, 0.f); */
|
||||
|
||||
auto camera = std::make_shared<se::Camera>(game.get_window(), core_shader, 70, glm::vec3(0, 0, 0));
|
||||
game.add_event(camera);
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "../../gfx/mesh.h"
|
||||
#include "../../event/event.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
namespace simpleengine {
|
||||
/**
|
||||
* @brief A Model is a object that will be shown on the screen by a renderer.
|
||||
*
|
||||
*/
|
||||
class Component : public simpleengine::Event {
|
||||
private:
|
||||
static uint32_t incrementing_handle;
|
||||
uint32_t handle;
|
||||
public:
|
||||
Component() {
|
||||
handle = incrementing_handle++;
|
||||
}
|
||||
|
||||
uint32_t get_handle() {
|
||||
return handle;
|
||||
}
|
||||
|
||||
virtual void update(const float& delta_time) override {
|
||||
std::cout << "Component update" << std::endl;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "component.h"
|
||||
#include "../../gfx/mesh.h"
|
||||
#include "../../gfx/material.h"
|
||||
|
||||
|
@ -9,37 +8,32 @@
|
|||
|
||||
namespace simpleengine {
|
||||
/**
|
||||
* @brief A Model is a object that will be shown on the screen by a renderer.
|
||||
* @brief A component that contains a Mesh that will be rendered.
|
||||
*
|
||||
*/
|
||||
class MeshComponent : public simpleengine::Component {
|
||||
class MeshComponent {
|
||||
public:
|
||||
gfx::Mesh model;
|
||||
//gfx::Material material;
|
||||
gfx::Mesh mesh;
|
||||
|
||||
MeshComponent(gfx::Mesh model) : model(model) {
|
||||
MeshComponent(gfx::Mesh mesh) : mesh(mesh) {
|
||||
|
||||
}
|
||||
|
||||
MeshComponent(std::vector<LitVertex> vertices, std::vector<GLuint> indicies, gfx::Material material,
|
||||
bool calculate_normals = false): model(vertices, indicies, material) {
|
||||
bool calculate_normals = false): mesh(vertices, indicies, material) {
|
||||
|
||||
if (calculate_normals) {
|
||||
model.calculate_normals();
|
||||
mesh.calculate_normals();
|
||||
}
|
||||
}
|
||||
|
||||
MeshComponent(std::vector<LitVertex> vertices, std::vector<GLuint> indicies = std::vector<GLuint>(),
|
||||
std::optional<gfx::Material> material = std::nullopt, bool calculate_normals = false) :
|
||||
model(vertices, indicies, material) {
|
||||
mesh(vertices, indicies, material) {
|
||||
|
||||
if (calculate_normals) {
|
||||
model.calculate_normals();
|
||||
mesh.calculate_normals();
|
||||
}
|
||||
}
|
||||
|
||||
virtual void update(const float& delta_time) override {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "component.h"
|
||||
#include "../../gfx/model.h"
|
||||
|
||||
#include <iostream>
|
||||
|
@ -8,10 +7,10 @@
|
|||
|
||||
namespace simpleengine {
|
||||
/**
|
||||
* @brief A Model is a object that will be shown on the screen by a renderer.
|
||||
* @brief A component that contains a Model that will be rendered.
|
||||
*
|
||||
*/
|
||||
class ModelComponent : public simpleengine::Component {
|
||||
class ModelComponent {
|
||||
public:
|
||||
gfx::Model model;
|
||||
|
||||
|
@ -22,9 +21,5 @@ namespace simpleengine {
|
|||
ModelComponent(std::string model_file_path) : model(model_file_path) {
|
||||
|
||||
}
|
||||
|
||||
virtual void update(const float& delta_time) override {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
#pragma once
|
||||
|
||||
#include <glm/ext/matrix_transform.hpp>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
namespace simpleengine {
|
||||
/**
|
||||
* @brief A component that contains a Mesh that will be rendered.
|
||||
*
|
||||
*/
|
||||
class TransformComponent {
|
||||
public:
|
||||
glm::mat4 transform_matrix;
|
||||
|
||||
TransformComponent() : transform_matrix(glm::mat4(1.f)) {
|
||||
|
||||
}
|
||||
|
||||
TransformComponent(glm::mat4 transform_matrix) : transform_matrix(transform_matrix) {
|
||||
|
||||
}
|
||||
|
||||
friend TransformComponent operator+(TransformComponent lhs, const TransformComponent& rhs) {
|
||||
lhs.transform_matrix += rhs.transform_matrix;
|
||||
return lhs;
|
||||
}
|
||||
|
||||
friend TransformComponent operator-(TransformComponent lhs, const TransformComponent& rhs) {
|
||||
lhs.transform_matrix -= rhs.transform_matrix;
|
||||
return lhs;
|
||||
}
|
||||
|
||||
friend TransformComponent operator*(TransformComponent lhs, const TransformComponent& rhs) {
|
||||
lhs.transform_matrix *= rhs.transform_matrix;
|
||||
return lhs;
|
||||
}
|
||||
|
||||
friend TransformComponent operator/(TransformComponent lhs, const TransformComponent& rhs) {
|
||||
lhs.transform_matrix /= rhs.transform_matrix;
|
||||
return lhs;
|
||||
}
|
||||
|
||||
virtual void combine_transform(const glm::mat4& transform_matrix) {
|
||||
this->transform_matrix *= transform_matrix;
|
||||
}
|
||||
|
||||
virtual void combine_transform(const TransformComponent& transformable) {
|
||||
transform_matrix = transformable.transform_matrix;
|
||||
}
|
||||
|
||||
virtual void translate(float x, float y, float z) {
|
||||
transform_matrix = glm::translate(transform_matrix, glm::vec3(x, y, z));
|
||||
}
|
||||
|
||||
/* virtual void translate(const glm::vec3& vec) {
|
||||
transform_matrix = glm::translate(transform_matrix, vec);
|
||||
} */
|
||||
|
||||
virtual glm::mat4 rotation_matrix(float degrees, glm::vec3 rotation_axis) const {
|
||||
return glm::rotate(transform_matrix, glm::radians(degrees), rotation_axis);
|
||||
}
|
||||
|
||||
virtual glm::mat4 rotation_x_matrix(float degrees) const {
|
||||
return rotation_matrix(degrees, glm::vec3(1, 0, 0));
|
||||
}
|
||||
|
||||
virtual glm::mat4 rotation_y_matrix(float degrees) const {
|
||||
return rotation_matrix(degrees, glm::vec3(0, 1, 0));
|
||||
}
|
||||
|
||||
virtual glm::mat4 rotation_z_matrix(float degrees) const {
|
||||
return rotation_matrix(degrees, glm::vec3(0, 0, 1));
|
||||
}
|
||||
|
||||
virtual void rotate(float degrees, glm::vec3 rotation_axis) {
|
||||
transform_matrix = rotation_matrix(degrees, rotation_axis);
|
||||
}
|
||||
|
||||
virtual void rotate_x(float degrees) {
|
||||
transform_matrix = rotation_x_matrix(degrees);
|
||||
}
|
||||
|
||||
virtual void rotate_y(float degrees) {
|
||||
transform_matrix = rotation_y_matrix(degrees);
|
||||
}
|
||||
|
||||
virtual void rotate_z(float degrees) {
|
||||
transform_matrix = rotation_z_matrix(degrees);
|
||||
}
|
||||
|
||||
virtual void scale(glm::vec3 scalar_vec) {
|
||||
transform_matrix = glm::scale(transform_matrix, scalar_vec);
|
||||
}
|
||||
|
||||
virtual void scale(float scalar) {
|
||||
transform_matrix = glm::scale(transform_matrix, glm::vec3(scalar, scalar, scalar));
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,116 +1,266 @@
|
|||
#pragma once
|
||||
|
||||
#include "component/component.h"
|
||||
#include "../transformable.h"
|
||||
#include "../util.h"
|
||||
#include "entt/entity/fwd.hpp"
|
||||
#include <cstddef>
|
||||
#include <entt/entt.hpp>
|
||||
#include <utility>
|
||||
|
||||
#include <iterator>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <random>
|
||||
#include <bitset>
|
||||
#include <type_traits>
|
||||
|
||||
namespace simpleengine {
|
||||
/**
|
||||
* @brief A Model is a object that will be shown on the screen by a renderer.
|
||||
*
|
||||
*/
|
||||
class Entity : public simpleengine::Event, public simpleengine::Transformable {
|
||||
// TODO: Don't extend from Event, create own destroy function
|
||||
namespace simpleengine::ecs {
|
||||
class Entity {
|
||||
private:
|
||||
static uint32_t incrementing_handle;
|
||||
uint32_t handle;
|
||||
entt::registry& registry;
|
||||
entt::entity inner;
|
||||
public:
|
||||
std::vector<std::shared_ptr<Component>> components;
|
||||
|
||||
Entity() : components({}) {
|
||||
handle = incrementing_handle++;
|
||||
}
|
||||
|
||||
/* Entity() : components({}) {
|
||||
std::random_device rd; // obtain a random number from hardware
|
||||
std::mt19937 gen(rd()); // seed the generator
|
||||
std::uniform_int_distribution<uint16_t> distr(1, std::numeric_limits<std::uint16_t>::max());
|
||||
|
||||
uint16_t num = distr(gen);
|
||||
uint32_t pid = simpleengine::util::get_current_pid();
|
||||
|
||||
handle |= num;
|
||||
handle |= (pid << 16);
|
||||
|
||||
std::string binary = std::bitset<16>(num).to_string();
|
||||
std::cout << "Entity handle: " << binary << std::endl;
|
||||
/* Entity(entt::entity entity) : inner(entity) {
|
||||
|
||||
} */
|
||||
|
||||
Entity(std::vector<std::shared_ptr<Component>> components) : Entity() {
|
||||
this->components = components;
|
||||
Entity(entt::registry& registry, entt::entity entity) : registry(registry), inner(entity) {
|
||||
|
||||
}
|
||||
|
||||
uint32_t get_handle() {
|
||||
return handle;
|
||||
/**
|
||||
* @brief Checks if an identifier refers to a valid entity.
|
||||
*
|
||||
* @link https://skypjack.github.io/entt/classentt_1_1basic__registry.html#a1795b1221d728f806319a685930f520d
|
||||
*
|
||||
* @return true if the identifier is valid
|
||||
* @return false otherwise
|
||||
*/
|
||||
bool is_valid() const {
|
||||
return registry.valid(inner);
|
||||
}
|
||||
|
||||
virtual void update(const float& delta_time) override {
|
||||
for (auto& component : components) {
|
||||
component->update(delta_time);
|
||||
}
|
||||
|
||||
rotate_y(delta_time * 10); // TODO: Remove
|
||||
/**
|
||||
* @brief Returns the actual version for an identifier.
|
||||
*
|
||||
* @link https://skypjack.github.io/entt/classentt_1_1basic__registry.html#a199babc787d6baa6f7ccce761228a5f6
|
||||
*
|
||||
* @return entt::registry::version_type The version for the given identifier if valid, the tombstone version otherwise.
|
||||
*/
|
||||
entt::registry::version_type current_version() const {
|
||||
return registry.current(inner);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
bool has_component() const {
|
||||
for (const auto& comp : components) {
|
||||
if (std::dynamic_pointer_cast<T>(comp)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
/**
|
||||
* @brief Releases an identifier.
|
||||
*
|
||||
* The version is updated and the identifier can be recycled at any time.
|
||||
*
|
||||
* @link https://skypjack.github.io/entt/classentt_1_1basic__registry.html#a3d9cb2368384b0952cb54848e777359e
|
||||
*
|
||||
* @return entt::registry::version_type The version of the recycled entity.
|
||||
*/
|
||||
entt::registry::version_type release() {
|
||||
return registry.release(inner);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::shared_ptr<T> get_component() const {
|
||||
for (const auto& comp : components) {
|
||||
if (std::shared_ptr<T> dyn_comp = std::dynamic_pointer_cast<T>(comp); dyn_comp) {
|
||||
return dyn_comp;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
/**
|
||||
* @brief Releases an identifier.
|
||||
*
|
||||
* The suggested version or the valid version closest to the suggested one is used instead of the implicitly generated version.
|
||||
*
|
||||
* @link https://skypjack.github.io/entt/classentt_1_1basic__registry.html#af9c919867fc93a7e1a2d0762ac3a9877
|
||||
*
|
||||
* @param version A desired version upon destruction.
|
||||
* @return entt::registry::version_type The version actually assigned to the entity.
|
||||
*/
|
||||
entt::registry::version_type release(entt::registry::version_type version) {
|
||||
return registry.release(inner, version);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void add_component(std::shared_ptr<T> component) {
|
||||
static_assert(std::is_base_of_v<Component, T>, "Component class must derive from simpleengine::Component");
|
||||
|
||||
// Only allow one type of the same component
|
||||
assert(!has_component<T>()); // TODO: Don't assert, give an error
|
||||
components.push_back(component);
|
||||
/**
|
||||
* @brief Destroys an entity and releases its identifier.
|
||||
*
|
||||
* @warning Adding or removing components to an entity that is being destroyed can result in undefined behavior. Attempting to use an invalid entity results in undefined behavior.
|
||||
*
|
||||
* @link https://skypjack.github.io/entt/classentt_1_1basic__registry.html#a7b2c0368d508a6af2d094a9fc592a4a0
|
||||
*
|
||||
* @return entt::registry::version_type The version of the recycled entity.
|
||||
*/
|
||||
entt::registry::version_type destroy() {
|
||||
return registry.destroy(inner);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void add_component(T component) {
|
||||
static_assert(std::is_base_of_v<Component, T>, "Component class must derive from simpleengine::Component");
|
||||
|
||||
// Only allow one type of the same component
|
||||
assert(!has_component<T>()); // TODO: Don't assert, give an error
|
||||
components.push_back(std::make_shared<T>(component));
|
||||
/**
|
||||
* @brief Destroys an entity and releases its identifier.
|
||||
*
|
||||
* The suggested version or the valid version closest to the suggested one is used instead of the implicitly generated version.
|
||||
*
|
||||
* @link https://skypjack.github.io/entt/classentt_1_1basic__registry.html#adc175a0d3bcf83a133c63890c674ceb3
|
||||
*
|
||||
* @param version A desired version upon destruction.
|
||||
* @return entt::registry::version_type The version actually assigned to the entity.
|
||||
*/
|
||||
entt::registry::version_type destroy(entt::registry::version_type version) {
|
||||
return registry.destroy(inner, version);
|
||||
}
|
||||
|
||||
template<typename T, typename ...Args>
|
||||
std::shared_ptr<T> add_component(Args&&... args) {
|
||||
static_assert(std::is_base_of_v<Component, T>, "Component class must derive from simpleengine::Component");
|
||||
/**
|
||||
* @brief Assigns the given component to an entity.
|
||||
*
|
||||
* The component must have a proper constructor or be of aggregate type.
|
||||
*
|
||||
* @warning Attempting to use an invalid entity or to assign a component to an entity that already owns it results in undefined behavior.
|
||||
*
|
||||
* @link https://skypjack.github.io/entt/classentt_1_1basic__registry.html#a4c9c0532972adc7a930a50888a97efdf
|
||||
*
|
||||
* @tparam Component Type of component to create.
|
||||
* @tparam Args Types of arguments to use to construct the component.
|
||||
*
|
||||
* @param args Parameters to use to initialize the component.
|
||||
*
|
||||
* @return decltype(auto) A reference to the newly created component.
|
||||
*/
|
||||
template<typename Component, typename... Args>
|
||||
decltype(auto) add_component(Args&&... args) {
|
||||
return registry.emplace<Component>(inner, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
// Only allow one type of the same component
|
||||
assert(!has_component<T>()); // TODO: Don't assert, give an error
|
||||
auto comp = std::make_shared<T>(std::forward<Args>(args)...);
|
||||
components.push_back(comp);
|
||||
/**
|
||||
* @brief Assigns or replaces the given component for an entity.
|
||||
*
|
||||
* The component must have a proper constructor or be of aggregate type.
|
||||
*
|
||||
* @warning Attempting to use an invalid entity or to assign a component to an entity that already owns it results in undefined behavior.
|
||||
*
|
||||
* @link https://skypjack.github.io/entt/classentt_1_1basic__registry.html#a3bbaebbb9365eef262453e42a8f7dc4b
|
||||
*
|
||||
* @tparam Component Type of component to assign or replace.
|
||||
* @tparam Args Types of arguments to use to construct the component.
|
||||
*
|
||||
* @param args Parameters to use to initialize the component.
|
||||
*
|
||||
* @return decltype(auto) A reference to the newly created component.
|
||||
*/
|
||||
template<typename Component, typename... Args>
|
||||
decltype(auto) add_or_replace_component(Args&&... args) {
|
||||
return registry.emplace_or_replace<Component>(inner, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
return comp;
|
||||
/**
|
||||
* @brief Replaces the given component for an entity.
|
||||
*
|
||||
* The component must have a proper constructor or be of aggregate type.
|
||||
*
|
||||
* @warning Attempting to use an invalid entity or to replace a component of an entity that doesn't own it results in undefined behavior.
|
||||
*
|
||||
* @link https://skypjack.github.io/entt/classentt_1_1basic__registry.html#a0501ff5a96a3421f3eec427bf5d62380
|
||||
*
|
||||
* @tparam Component Type of component to replace.
|
||||
* @tparam Args Types of arguments to use to construct the component.
|
||||
*
|
||||
* @param args Parameters to use to initialize the component.
|
||||
*
|
||||
* @return decltype(auto) A reference to the component being replaced.
|
||||
*/
|
||||
template<typename Component, typename... Args>
|
||||
decltype(auto) replace_component(Args&&... args) {
|
||||
return registry.replace<Component>(inner, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Patches the given component for an entity.
|
||||
*
|
||||
* The signature of the function should be equivalent to the following:
|
||||
* \code{.cpp}
|
||||
* void(Component &);
|
||||
* \endcode
|
||||
*
|
||||
* @note Empty types aren't explicitly instantiated and therefore they are never returned. However, this function can be used to trigger an update signal for them.
|
||||
*
|
||||
* @warning Attempting to use an invalid entity or to patch a component of an entity that doesn't own it results in undefined behavior.
|
||||
*
|
||||
* @link https://skypjack.github.io/entt/classentt_1_1basic__registry.html#a22454253689ff77e41326f849bfea976
|
||||
*
|
||||
* @tparam Component Type of component to patch.
|
||||
* @tparam Func Types of the function objects to invoke.
|
||||
*
|
||||
* @param func Valid function objects.
|
||||
*
|
||||
* @return decltype(auto) A reference to the patched component.
|
||||
*/
|
||||
template<typename Component, typename... Func>
|
||||
decltype(auto) patch(Func&&... func) {
|
||||
return registry.patch<Component>(inner, std::forward<Func>(func)...);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Removes the given components from an entity.
|
||||
*
|
||||
* @warning Attempting to use an invalid entity results in undefined behavior.
|
||||
*
|
||||
* @link https://skypjack.github.io/entt/classentt_1_1basic__registry.html#aa66ca71c5eb6c2e1b3bde6ff86a268c7
|
||||
*
|
||||
* @tparam Component Type of component to remove.
|
||||
* @tparam Other Other types of components to remove.
|
||||
*
|
||||
* @return size_t The number of components actually removed.
|
||||
*/
|
||||
template<typename Component, typename... Other>
|
||||
size_t remove_components() {
|
||||
return registry.remove<Component, Other...>(inner);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Erases the given components from an entity.
|
||||
*
|
||||
* @warning Attempting to use an invalid entity or to erase a component from an entity that doesn't own it results in undefined behavior.
|
||||
*
|
||||
* @link https://skypjack.github.io/entt/classentt_1_1basic__registry.html#ad7a51ade7ff181d7aa0b35915d0a4f2b
|
||||
*
|
||||
* @tparam Component Types of components to erase.
|
||||
* @tparam Other Other types of components to erase.
|
||||
*/
|
||||
template<typename Component, typename... Other>
|
||||
void erase() {
|
||||
return registry.remove<Component, Other...>(inner);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks if an entity has all the given components.
|
||||
*
|
||||
* @warning Attempting to use an invalid entity results in undefined behavior.
|
||||
*
|
||||
* @link https://skypjack.github.io/entt/classentt_1_1basic__registry.html#a40072eca607846f43f47d0e3f11dd196
|
||||
*
|
||||
* @tparam Component Components for which to perform the check.
|
||||
*
|
||||
* @return true if the entity has all the components
|
||||
* @return false otherwise
|
||||
*/
|
||||
template<typename... Component>
|
||||
bool has_all_of() const {
|
||||
return registry.all_of<Component...>(inner);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks if an entity has at least one of the given components.
|
||||
*
|
||||
* @warning Attempting to use an invalid entity results in undefined behavior.
|
||||
*
|
||||
* @link https://skypjack.github.io/entt/classentt_1_1basic__registry.html#a08f819276fd2d8b2b2b6d01357f61a42
|
||||
*
|
||||
* @tparam Component Components for which to perform the check.
|
||||
*
|
||||
* @return true if the entity has at least one of the given components
|
||||
* @return false otherwise
|
||||
*/
|
||||
template<typename... Component>
|
||||
bool has_any_of() const {
|
||||
return registry.any_of<Component...>(inner);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks if an entity has components assigned.
|
||||
*
|
||||
* @link https://skypjack.github.io/entt/classentt_1_1basic__registry.html#a10f5e61d4cabab9e9c1e87edc30de551
|
||||
*
|
||||
* @return true if the entity has no components assigned
|
||||
* @return false otherwise
|
||||
*/
|
||||
bool has_any_components() const {
|
||||
return registry.orphan(inner);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "ecs/entity.h"
|
||||
#include "event/event.h"
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace simpleengine {
|
||||
class EntityManager : public simpleengine::Event {
|
||||
public:
|
||||
std::unordered_map<uint32_t, std::shared_ptr<simpleengine::Entity>> entities;
|
||||
|
||||
EntityManager();
|
||||
|
||||
virtual void submit_entity(std::shared_ptr<simpleengine::Entity> entity);
|
||||
virtual bool withdraw_entity(std::shared_ptr<simpleengine::Entity> entity);
|
||||
|
||||
virtual void initialize();
|
||||
virtual void destroy() override;
|
||||
|
||||
virtual void update(const float& delta_time) override;
|
||||
};
|
||||
}
|
|
@ -23,6 +23,7 @@ namespace simpleengine::gfx {
|
|||
std::vector<LitVertex> vertices;
|
||||
std::vector<GLuint> indicies;
|
||||
|
||||
bool are_buffers_created = false;
|
||||
gfx::VBO ebo;
|
||||
gfx::VBO vbo;
|
||||
gfx::VAO vao;
|
||||
|
|
|
@ -1,45 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include "../ecs/entity.h"
|
||||
#include "material.h"
|
||||
#include "texture.h"
|
||||
#include "shader.h"
|
||||
//#include "renderable.h"
|
||||
#include "mesh.h"
|
||||
#include "model.h"
|
||||
#include "simpleengine/gfx/mesh.h"
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <queue>
|
||||
|
||||
namespace simpleengine::gfx {
|
||||
class RenderingJob {
|
||||
public:
|
||||
gfx::Mesh& rendering_mesh;
|
||||
glm::mat4 transform_mat;
|
||||
|
||||
RenderingJob(gfx::Mesh& mesh, glm::mat4 position) : rendering_mesh(mesh), transform_mat(position) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
class Renderer : public simpleengine::Renderable {
|
||||
private:
|
||||
GLFWwindow* window;
|
||||
public:
|
||||
|
||||
class RenderingModel {
|
||||
public:
|
||||
std::shared_ptr<simpleengine::Entity> entity;
|
||||
std::unordered_map<uint32_t, gfx::Mesh&> component_models;
|
||||
|
||||
RenderingModel(std::shared_ptr<simpleengine::Entity> entity) : entity(entity) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create and delete buffers for new and old components in entity.
|
||||
*
|
||||
*/
|
||||
void update_buffers();
|
||||
|
||||
/**
|
||||
* @brief Destroy the buffers
|
||||
*
|
||||
*/
|
||||
void destroy_buffers();
|
||||
};
|
||||
|
||||
std::unordered_map<uint32_t, RenderingModel> rendering_models;
|
||||
std::queue<RenderingJob> rendering_queue;
|
||||
gfx::Shader shader;
|
||||
|
||||
Renderer(GLFWwindow* window, gfx::Shader shader);
|
||||
|
@ -47,8 +31,8 @@ namespace simpleengine::gfx {
|
|||
|
||||
void enable_debug();
|
||||
|
||||
virtual void submit_entity(std::shared_ptr<simpleengine::Entity> entity);
|
||||
virtual bool withdraw_entity(std::shared_ptr<simpleengine::Entity> entity);
|
||||
virtual void queue_job(RenderingJob job);
|
||||
virtual void create_job_buffers(RenderingJob& job);
|
||||
|
||||
virtual void initialize();
|
||||
virtual void destroy() override;
|
||||
|
|
|
@ -1,44 +1,32 @@
|
|||
#pragma once
|
||||
|
||||
#include "entt/entity/fwd.hpp"
|
||||
#include "gfx/mesh.h"
|
||||
#include "entity.h"
|
||||
#include "event/event.h"
|
||||
#include "renderable.h"
|
||||
#include "simpleengine/gfx/renderer.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <vector>
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
namespace simpleengine {
|
||||
namespace ecs {
|
||||
class Entity;
|
||||
}
|
||||
|
||||
class Scene : public simpleengine::Event {
|
||||
protected:
|
||||
entt::registry registry;
|
||||
std::shared_ptr<gfx::Renderer> renderer;
|
||||
public:
|
||||
/**
|
||||
* @brief A list of entities in this scene.
|
||||
*
|
||||
*/
|
||||
std::vector<std::shared_ptr<Entity>> entities;
|
||||
Scene(std::shared_ptr<gfx::Renderer> renderer);
|
||||
|
||||
/**
|
||||
* @brief Models that don't belong to an entity.
|
||||
*
|
||||
*/
|
||||
std::vector<std::shared_ptr<gfx::Model>> stray_models;
|
||||
ecs::Entity create_entity();
|
||||
|
||||
Scene() = default;
|
||||
|
||||
void add_entity(std::shared_ptr<Entity> entity) {
|
||||
entities.push_back(entity);
|
||||
}
|
||||
|
||||
void add_stray_model(std::shared_ptr<gfx::Model> stray) {
|
||||
stray_models.push_back(stray);
|
||||
}
|
||||
|
||||
virtual void update(const float& delta_time) override {
|
||||
for (auto& entity : entities) {
|
||||
entity->update(delta_time);
|
||||
}
|
||||
}
|
||||
virtual void update(const float& delta_time) override;
|
||||
};
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
#include "ecs/component/component.h"
|
||||
|
||||
uint32_t simpleengine::Component::incrementing_handle = 0;
|
|
@ -1,3 +0,0 @@
|
|||
#include "ecs/entity.h"
|
||||
|
||||
uint32_t simpleengine::Entity::incrementing_handle = 0;
|
|
@ -1,40 +0,0 @@
|
|||
#include "entity_manager.h"
|
||||
|
||||
simpleengine::EntityManager::EntityManager() {
|
||||
|
||||
}
|
||||
|
||||
void simpleengine::EntityManager::submit_entity(std::shared_ptr<simpleengine::Entity> entity) {
|
||||
entities.emplace(entity->get_handle(), entity);
|
||||
}
|
||||
|
||||
bool simpleengine::EntityManager::withdraw_entity(std::shared_ptr<simpleengine::Entity> entity) {
|
||||
auto it = entities.find(entity->get_handle());
|
||||
|
||||
if (it != entities.end()) {
|
||||
it->second->destroy();
|
||||
entities.erase(it);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void simpleengine::EntityManager::initialize() {
|
||||
|
||||
}
|
||||
|
||||
void simpleengine::EntityManager::destroy() {
|
||||
std::cout << "Destroy entity manager!" << std::endl;
|
||||
|
||||
for (auto& [handle, entity] : entities) {
|
||||
entity->destroy();
|
||||
}
|
||||
}
|
||||
|
||||
void simpleengine::EntityManager::update(const float& delta_time) {
|
||||
for (auto& [handle, entity] : entities) {
|
||||
entity->update(delta_time);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
#include "gfx/renderer.h"
|
||||
#include "ecs/component/component.h"
|
||||
#include "ecs/entity.h"
|
||||
#include "gfx/mesh.h"
|
||||
#include "gfx/vao.h"
|
||||
#include "renderable.h"
|
||||
|
@ -12,16 +10,7 @@
|
|||
#include <assimp/material.h>
|
||||
|
||||
namespace simpleengine::gfx {
|
||||
void create_mesh_buffers(std::shared_ptr<simpleengine::Component> comp, simpleengine::gfx::Mesh& mesh);
|
||||
|
||||
void Renderer::RenderingModel::destroy_buffers() {
|
||||
std::cout << "Destroying entity models..." << std::endl;
|
||||
|
||||
// Iterate through all buffer lists and destroy each inner buffer.
|
||||
for (auto& pair : component_models) {
|
||||
pair.second.destroy();
|
||||
}
|
||||
}
|
||||
void create_mesh_buffers(simpleengine::gfx::Mesh& mesh);
|
||||
|
||||
Renderer::Renderer(GLFWwindow* window, gfx::Shader shader): window(window), shader(shader) {
|
||||
|
||||
|
@ -45,24 +34,40 @@ namespace simpleengine::gfx {
|
|||
glDebugMessageCallback(debug_message_callback, 0);
|
||||
}
|
||||
|
||||
void Renderer::submit_entity(std::shared_ptr<simpleengine::Entity> entity) {
|
||||
std::cout << "Submitting entity (" << entity->get_handle() << ")..." << std::endl;
|
||||
auto it = rendering_models.emplace(entity->get_handle(), entity);
|
||||
it.first->second.update_buffers();
|
||||
void Renderer::queue_job(RenderingJob job) {
|
||||
RenderingJob& emplace = rendering_queue.emplace(job);
|
||||
create_job_buffers(emplace);
|
||||
}
|
||||
|
||||
bool Renderer::withdraw_entity(std::shared_ptr<simpleengine::Entity> entity) {
|
||||
std::cout << "Withdrawing entity (" << entity->get_handle() << ")...";
|
||||
auto it = rendering_models.find(entity->get_handle());
|
||||
void Renderer::create_job_buffers(RenderingJob& job) {
|
||||
Mesh& rendering_mesh = job.rendering_mesh;
|
||||
|
||||
if (it != rendering_models.end()) {
|
||||
it->second.destroy_buffers();
|
||||
rendering_models.erase(it);
|
||||
if (!rendering_mesh.are_buffers_created) {
|
||||
gfx::VBO& vbo = rendering_mesh.vbo;
|
||||
gfx::VBO& ebo = rendering_mesh.ebo;
|
||||
gfx::VAO& vao = rendering_mesh.vao;
|
||||
|
||||
return true;
|
||||
vao.bind();
|
||||
vbo.buffer(rendering_mesh.vertices.data(), 0, sizeof(LitVertex) * rendering_mesh.vertices.size());
|
||||
|
||||
if (!rendering_mesh.indicies.empty()) {
|
||||
ebo.buffer(rendering_mesh.indicies.data(), 0, rendering_mesh.indicies.size() * sizeof(GLuint));
|
||||
}
|
||||
|
||||
// Enable VAO attributes
|
||||
vao.enable_attrib(vbo, 0, 3, GL_FLOAT, sizeof(LitVertex), offsetof(LitVertex, position), false);
|
||||
vao.enable_attrib(vbo, 1, 3, GL_FLOAT, sizeof(LitVertex), offsetof(LitVertex, color), false);
|
||||
vao.enable_attrib(vbo, 2, 3, GL_FLOAT, sizeof(LitVertex), offsetof(LitVertex, normal), false);
|
||||
vao.enable_attrib(vbo, 3, 2, GL_FLOAT, sizeof(LitVertex), offsetof(LitVertex, tex_coord), false);
|
||||
vao.enable_attrib(vbo, 4, 1, GL_FLOAT, sizeof(LitVertex), offsetof(LitVertex, texture_id), false);
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindVertexArray(0);
|
||||
|
||||
rendering_mesh.are_buffers_created = true;
|
||||
|
||||
std::cout << "Created render job buffers" << std::endl;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Renderer::update(const float& delta_time) {
|
||||
|
@ -76,113 +81,53 @@ namespace simpleengine::gfx {
|
|||
void Renderer::destroy() {
|
||||
std::cout << "Destroying renderer..." << std::endl;
|
||||
|
||||
for (auto& [handle, rendering] : rendering_models) {
|
||||
/* for (auto& [handle, rendering] : rendering_models) {
|
||||
rendering.destroy_buffers();
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
void Renderer::render() {
|
||||
shader.use();
|
||||
|
||||
for (auto& [handle, rendering] : rendering_models) {
|
||||
if (rendering.component_models.size() >= 0) {
|
||||
std::shared_ptr<Entity>& entity = rendering.entity;
|
||||
while (!rendering_queue.empty()) {
|
||||
// Get the job from the queue, we'll remove it after we render.
|
||||
RenderingJob& job = rendering_queue.front();
|
||||
Mesh& mesh = job.rendering_mesh;
|
||||
|
||||
shader.set_uniform_matrix_4f("transform_matrix", entity->transform_matrix, false);
|
||||
shader.set_uniform_matrix_4f("transform_matrix", job.transform_mat, false);
|
||||
|
||||
for (const auto& pair : rendering.component_models) {
|
||||
Mesh& model = pair.second;
|
||||
std::optional<Material>& material = model.material;
|
||||
std::optional<Material>& material = mesh.material;
|
||||
|
||||
shader.set_uniform_int("u_textures", 0, false);
|
||||
shader.set_uniform_int("u_textures", 0, false);
|
||||
|
||||
if (material.has_value()) {
|
||||
shader.set_uniform_float("u_texture_shine", material->shine, false);
|
||||
shader.set_uniform_float("u_texture_reflectivity", material->reflectivity, false);
|
||||
if (material.has_value()) {
|
||||
shader.set_uniform_float("u_texture_shine", material->shine, false);
|
||||
shader.set_uniform_float("u_texture_reflectivity", material->reflectivity, false);
|
||||
|
||||
int texture_count = 0;
|
||||
auto diffuse_maps = material->textures.find(aiTextureType_DIFFUSE);
|
||||
for (const auto& texture : diffuse_maps->second) {
|
||||
// We can only bind to 16 textures at a time (indexes are 0-15)
|
||||
if (texture_count >= 16) break;
|
||||
int texture_count = 0;
|
||||
auto diffuse_maps = material->textures.find(aiTextureType_DIFFUSE);
|
||||
for (const auto& texture : diffuse_maps->second) {
|
||||
// We can only bind to 16 textures at a time (indexes are 0-15)
|
||||
if (texture_count >= 16) break;
|
||||
|
||||
glActiveTexture(GL_TEXTURE0 + texture_count);
|
||||
glBindTextureUnit(texture_count, texture.get_texture_id());
|
||||
glActiveTexture(GL_TEXTURE0 + texture_count);
|
||||
glBindTextureUnit(texture_count, texture.get_texture_id());
|
||||
|
||||
texture_count++;
|
||||
}
|
||||
}
|
||||
|
||||
model.vao.bind();
|
||||
if (model.indicies.empty()) {
|
||||
glDrawArrays(GL_TRIANGLES, 0, model.vertices.size());
|
||||
} else {
|
||||
glDrawElements(GL_TRIANGLES, model.indicies.size(), GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
texture_count++;
|
||||
}
|
||||
}
|
||||
|
||||
mesh.vao.bind();
|
||||
if (mesh.indicies.empty()) {
|
||||
glDrawArrays(GL_TRIANGLES, 0, mesh.vertices.size());
|
||||
} else {
|
||||
glDrawElements(GL_TRIANGLES, mesh.indicies.size(), GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
|
||||
// Now we'll remove the job from the queue.
|
||||
rendering_queue.pop();
|
||||
}
|
||||
|
||||
shader.unuse();
|
||||
}
|
||||
|
||||
void Renderer::RenderingModel::update_buffers() {
|
||||
if (entity->has_component<simpleengine::MeshComponent>()) {
|
||||
std::shared_ptr<MeshComponent> comp = entity->get_component<simpleengine::MeshComponent>();
|
||||
auto iter = component_models.find(comp->get_handle());
|
||||
if (iter == component_models.end()) {
|
||||
std::cout << "Enabling buffer attributes for MeshComponent (" << comp->get_handle() << ")..." << std::endl;
|
||||
|
||||
//iter->second = comp->model;
|
||||
gfx::Mesh& mesh = comp->model;
|
||||
create_mesh_buffers(comp, mesh);
|
||||
component_models.emplace(comp->get_handle(), mesh);
|
||||
|
||||
std::cout << "Enabled all buffer attributes for MeshComponent" << std::endl;
|
||||
} else {
|
||||
std::cout << "Already exists" << std::endl;
|
||||
}
|
||||
} else if (entity->has_component<simpleengine::ModelComponent>()) {
|
||||
std::shared_ptr<ModelComponent> comp = entity->get_component<simpleengine::ModelComponent>();
|
||||
|
||||
auto iter = component_models.find(comp->get_handle());
|
||||
if (iter == component_models.end()) {
|
||||
std::cout << "Enabling buffer attributes for ModelComponent (" << comp->get_handle() << ")..." << std::endl;
|
||||
|
||||
// Store all the model's meshes
|
||||
for (auto& mesh : comp->model.meshes) {
|
||||
create_mesh_buffers(comp, mesh);
|
||||
|
||||
component_models.emplace(comp->get_handle(), mesh);
|
||||
}
|
||||
|
||||
std::cout << "Enabled all buffer attributes for ModelComponent" << std::endl;
|
||||
} else {
|
||||
std::cout << "Already exists" << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void create_mesh_buffers(std::shared_ptr<Component> comp, gfx::Mesh& mesh) {
|
||||
gfx::VBO& vbo = mesh.vbo;
|
||||
gfx::VBO& ebo = mesh.ebo;
|
||||
gfx::VAO& vao = mesh.vao;
|
||||
|
||||
vao.bind();
|
||||
vbo.buffer(mesh.vertices.data(), 0, sizeof(LitVertex) * mesh.vertices.size());
|
||||
|
||||
if (!mesh.indicies.empty()) {
|
||||
ebo.buffer(mesh.indicies.data(), 0, mesh.indicies.size() * sizeof(GLuint));
|
||||
}
|
||||
|
||||
// Enable VAO attributes
|
||||
vao.enable_attrib(vbo, 0, 3, GL_FLOAT, sizeof(LitVertex), offsetof(LitVertex, position), false);
|
||||
vao.enable_attrib(vbo, 1, 3, GL_FLOAT, sizeof(LitVertex), offsetof(LitVertex, color), false);
|
||||
vao.enable_attrib(vbo, 2, 3, GL_FLOAT, sizeof(LitVertex), offsetof(LitVertex, normal), false);
|
||||
vao.enable_attrib(vbo, 3, 2, GL_FLOAT, sizeof(LitVertex), offsetof(LitVertex, tex_coord), false);
|
||||
vao.enable_attrib(vbo, 4, 1, GL_FLOAT, sizeof(LitVertex), offsetof(LitVertex, texture_id), false);
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
#include "scene.h"
|
||||
#include "ecs/component/mesh_component.h"
|
||||
#include "ecs/component/model_component.h"
|
||||
#include "ecs/component/transform_component.h"
|
||||
#include "ecs/entity.h"
|
||||
#include "gfx/renderer.h"
|
||||
|
||||
namespace simpleengine {
|
||||
Scene::Scene(std::shared_ptr<gfx::Renderer> renderer) : renderer(renderer) {
|
||||
|
||||
}
|
||||
|
||||
ecs::Entity Scene::create_entity() {
|
||||
return ecs::Entity(registry, registry.create());
|
||||
}
|
||||
|
||||
void Scene::update(const float& delta_time) {
|
||||
// Is there a way these can be grouped?
|
||||
registry.view<const TransformComponent, ModelComponent>().each([this](const TransformComponent& transform, ModelComponent& model_component) {
|
||||
for (auto& mesh : model_component.model.meshes) {
|
||||
renderer->queue_job(gfx::RenderingJob(mesh, transform.transform_matrix));
|
||||
}
|
||||
});
|
||||
|
||||
registry.view<const TransformComponent, MeshComponent>().each([this](const TransformComponent& transform, MeshComponent& mesh_component) {
|
||||
renderer->queue_job(gfx::RenderingJob(mesh_component.mesh, transform.transform_matrix));
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue