跳过正文

Metatron Dev. VI: ecs & serde

·1942 字·4 分钟· loading · loading ·
Graphics Rendering Metatron
目录

利用EnTTGlaze构建了一套场景管理系统.

ECS
#

metatron的ecs系统中, entity/component由entt管理, system则需要实现ecs::Daemon接口.

ecs::Hierarchy用于维护世界树, 树节点采用Unix路径命名并分配对应的ecs::Entity. hierarchy会包含多个ecs::Stage, stage中则包含多个ecs::Daemon, stage会按序更新, 但内部的daemon可能会由于多线程不能保证有序, 若需有序需位于不同的stage.

// ...
resource_stage->daemons = {
    &transform_daemon,
    &shape_daemon,
    &medium_daemon,
    &texture_daemon,
};
// ...
hierarchy.stages = {
    spectrum_stage.get(),
    resource_stage.get(),
    material_stage.get(),
    camera_stage.get(),
    render_stage.get(),
};

各个daemon会有initupdate接口, init用于提前挂载预定义的components(例如金属的IOR光谱)以及注册serde, update则执行前述的更新操作, 负载重的任务可通过stl::scheduler实现多线程.

struct Daemon final: pro::facade_builder
::add_convention<daemon_init, auto () noexcept -> void>
::add_convention<daemon_update, auto () noexcept -> void>
::add_skill<pro::skills::as_view>
::build {};

entt不会记录component是否被修改, metatron通过在世界树节点上挂载额外的ecs::Dirty_Mark<T>实现, 各个daemon更新完毕后需要清除.

template<typename T>
auto attach(Entity entity, T&& component = {}) noexcept -> void {
    registry.emplace<Dirty_Mark<T>>(entity);
    registry.emplace<T>(entity, std::forward<T>(component));
}

当前活跃的hierarchy可以通过global static ecs::Hierarchy::instance访问, 执行activate()来修改instance. 这主要用于方便entity与路径的转换, 例如可以通过字面量_et执行转换.

namespace mtt::ecs {
	auto to_path(Entity entity) -> std::string {
		return Hierarchy::instance->path(entity);
	}

	auto to_entity(std::string const& path) -> Entity {
		return Hierarchy::instance->create(path);
	}
}

namespace mtt {
	auto operator"" _et(view<char> path, usize size) -> ecs::Entity {
		return ecs::to_entity(path);
	}
}

SERDE
#

Glaze库通过调用编译器的internal函数实现反射功能, 对于aggregate的反射无需手动注册, 对部分容器也有支持, 在c++26的静态反射实装前是很好用的替代品.

对于ecs::Entity, 我们通过全局的ecs::Hierarchy::instance调用ecs::to_pathecs::to_entity即可. 对于metatron中最基础的math::Matrix类型, 注册为反射内部实际存储数据的std::array<Element, first_dim>, Glaze会处理Element的递归. math::Quaternion的反射同理. 除这三个类型外别的都可以交给Glaze处理.

template<typename T, mtt::usize first_dim, mtt::usize... rest_dims>
struct from<JSON, mtt::math::Matrix<T, first_dim, rest_dims...>> {
    template<auto Opts>
    auto static op(mtt::math::Matrix<T, first_dim, rest_dims...>& v, auto&&... args) noexcept -> void {
        using M = mtt::math::Matrix<T, first_dim, rest_dims...>;
        using E = M::Element;
        auto data = std::array<E, first_dim>{};
        parse<JSON>::op<Opts>(data, args...);
        v = M{std::span<E const>{data}};
    }
};

template<typename T, mtt::usize first_dim, mtt::usize... rest_dims>
struct to<JSON, mtt::math::Matrix<T, first_dim, rest_dims...>> {
    template<auto Opts>
    auto static op(mtt::math::Matrix<T, first_dim, rest_dims...> const& v, auto&&... args) noexcept -> void {
        using E = mtt::math::Matrix<T, first_dim, rest_dims...>::Element;
        auto const& data = std::array<E, first_dim>(v);
        serialize<JSON>::op<Opts>(data, args...);
    }
};

对于enum, Glaze需要手动注册才能反射出各项名称, 如果后续添加c++26支持的话理论上是不需要的, 或者说实现类似magic_enum的功能, 两种方法都会方便很多.

template<>
struct glz::meta<mtt::color::Color_Space::Spectrum_Type> {
	using enum mtt::color::Color_Space::Spectrum_Type;
	auto constexpr static value = glz::enumerate(
		albedo,
		unbounded,
		illuminant
	);
};

对于需要多态的components, 例如不同的光源, metatron通过std::variant实现, 避免同时存储各个子component的数据. 每个子component最后的i32用于Glaze序列化时标记, 否则需要为所有类型手动注册glz::meta::value, 并为std::variant<Ts...>注册glz::meta::tagglz::meta::ids.

struct Parallel_Light final {
    ecs::Entity spectrum;
    i32 parallel{0};
};

struct Point_Light final {
    ecs::Entity spectrum;
    i32 point{0};
};

struct Spot_Light final {
    ecs::Entity spectrum;
    f32 falloff_start_theta;
    f32 falloff_end_theta;
    i32 spot{0};
};

struct Environment_Light final {
    ecs::Entity env_map;
    i32 environment{0};
};

using Light = std::variant<
    Parallel_Light,
    Point_Light,
    Spot_Light,
    Environment_Light
>;

需要注意的是对于多重std::variant(std::variant<std::variant<Ts...>, std::varaint<Us...>>)Glaze是无法正确反序列化的, 原本metatron中有这种写法, 因为texture需要分为spectrum_texture与vector_texture, 后面还是展开到单个std::variant中, 通过实现stl::is_variant_alternative判断是否位于原本的内部std::variant中, 不再走std::visit来判断.

template<typename T, typename Variant>
struct is_variant_alternative : std::false_type {};

template<typename T, typename... Types>
struct is_variant_alternative<T, std::variant<Types...>>
    : std::disjunction<std::is_same<T, Types>...> {};

template<typename T, typename Variant>
auto constexpr is_variant_alternative_v = is_variant_alternative<T, Variant>::value;

json序列化结构如下, 每个daemon会注册处理序列化与反序列化的函数, 通过type索引到对应的std::function.

{
    "entity": "/divider/cloud",
    "type": "divider",
    "serialized": {
        "shape": "/hierarchy/shape/bound",
        "medium": "/hierarchy/medium/cloud",
        "material": "/material/cloud"
    }
}

注册的函数在ecs::Hierarchy中通过模板实现, 各个daemon提供类型即可, 类型名会通过宏MTT_SERDE(T)获取.

template<typename T>
auto static serde(std::string const& type) noexcept -> void {
    auto sanitized_type = type;
    std::ranges::transform(
        sanitized_type,
        sanitized_type.begin(),
        ::tolower
    );

    auto fr = [](ecs::Entity e, glz::raw_json const& s) -> void {
        auto d = T{};
        if (auto er = glz::read_json<T>(d, s.str); er) {
            std::println("desrialize {} with glaze error: {}", s.str, glz::format_error(er));
            std::abort();
        } else {
            ecs::Hierarchy::instance->attach(e, std::move(d));
        }
    };
    auto fw = [sanitized_type]() -> std::vector<serde::json> {
        auto v = std::vector<serde::json>{};
        auto& r = ecs::Hierarchy::instance->registry;
        for (auto e: r.view<T>()) {
            auto s = glz::write_json(r.get<T>(e));
            if (!s) {
                std::println(
                    "failed to serialize component {} on {}",
                    sanitized_type, ecs::Hierarchy::instance->path(e)
                );
                std::abort();
            }
            v.emplace_back(e, sanitized_type, s.value());
        }
        return v;
    };
    ecs::Hierarchy::instance->enable(sanitized_type, fr, fw);
}