fix: material serialization
This commit is contained in:
@@ -119,6 +119,7 @@ set(SRC_FILES
|
|||||||
"src/Serialization/SceneSerializer.cpp"
|
"src/Serialization/SceneSerializer.cpp"
|
||||||
"src/Serialization/ObjectSerialization.cpp"
|
"src/Serialization/ObjectSerialization.cpp"
|
||||||
"src/Serialization/PrefabSerializer.cpp"
|
"src/Serialization/PrefabSerializer.cpp"
|
||||||
|
"src/Serialization/MaterialSerialization.cpp"
|
||||||
"src/Serialization/PrefabCache.cpp"
|
"src/Serialization/PrefabCache.cpp"
|
||||||
"src/Serialization/ComponentRegistry.cpp"
|
"src/Serialization/ComponentRegistry.cpp"
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ public:
|
|||||||
|
|
||||||
[[nodiscard]] std::filesystem::path GetCookedPathForFile(std::string_view vpath) const;
|
[[nodiscard]] std::filesystem::path GetCookedPathForFile(std::string_view vpath) const;
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<std::string> ToVirtualPath(const std::filesystem::path& fullPath) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static std::vector<uint8_t> ReadFile(const std::filesystem::path& fullPath);
|
static std::vector<uint8_t> ReadFile(const std::filesystem::path& fullPath);
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ class ImageCache {
|
|||||||
friend class ResourcesInspector;
|
friend class ResourcesInspector;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
struct LoadedImageInfo {
|
||||||
|
std::filesystem::path path;
|
||||||
|
TextureIntent intent;
|
||||||
|
VkImageUsageFlags usage;
|
||||||
|
bool mipMap;
|
||||||
|
};
|
||||||
|
|
||||||
explicit ImageCache(GfxDevice& gfxDevice);
|
explicit ImageCache(GfxDevice& gfxDevice);
|
||||||
|
|
||||||
ImageID loadImageFromFile(
|
ImageID loadImageFromFile(
|
||||||
@@ -29,6 +36,7 @@ public:
|
|||||||
ImageID addImage(GPUImage image);
|
ImageID addImage(GPUImage image);
|
||||||
ImageID addImage(ImageID id, GPUImage image);
|
ImageID addImage(ImageID id, GPUImage image);
|
||||||
[[nodiscard]] const GPUImage& getImage(ImageID id) const;
|
[[nodiscard]] const GPUImage& getImage(ImageID id) const;
|
||||||
|
[[nodiscard]] const LoadedImageInfo* findLoadedImageInfo(ImageID id) const;
|
||||||
void removeImage(ImageID id);
|
void removeImage(ImageID id);
|
||||||
|
|
||||||
[[nodiscard]] ImageID getFreeImageId() const;
|
[[nodiscard]] ImageID getFreeImageId() const;
|
||||||
@@ -57,12 +65,6 @@ private:
|
|||||||
std::shared_ptr<std::vector<ImageID>> freeIds = std::make_shared<std::vector<ImageID>>();
|
std::shared_ptr<std::vector<ImageID>> freeIds = std::make_shared<std::vector<ImageID>>();
|
||||||
GfxDevice& gfxDevice;
|
GfxDevice& gfxDevice;
|
||||||
|
|
||||||
struct LoadedImageInfo {
|
|
||||||
std::filesystem::path path;
|
|
||||||
TextureIntent intent;
|
|
||||||
VkImageUsageFlags usage;
|
|
||||||
bool mipMap;
|
|
||||||
};
|
|
||||||
std::unordered_map<ImageID, LoadedImageInfo> loadedImagesInfo;
|
std::unordered_map<ImageID, LoadedImageInfo> loadedImagesInfo;
|
||||||
ImageID errorImageId{NULL_IMAGE_ID};
|
ImageID errorImageId{NULL_IMAGE_ID};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public:
|
|||||||
|
|
||||||
void cleanup(GfxDevice& gfxDevice);
|
void cleanup(GfxDevice& gfxDevice);
|
||||||
|
|
||||||
MaterialID addMaterial(Material material);
|
MaterialID addMaterial(Material material, std::string key = {});
|
||||||
MaterialID addSimpleColorMaterial(
|
MaterialID addSimpleColorMaterial(
|
||||||
glm::vec3 color,
|
glm::vec3 color,
|
||||||
std::string name = {});
|
std::string name = {});
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#ifndef DESTRUM_MATERIALSERIALIZATION_H
|
||||||
|
#define DESTRUM_MATERIALSERIALIZATION_H
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/Material.h>
|
||||||
|
|
||||||
|
namespace MaterialSerialization {
|
||||||
|
using TextureWriter = std::function<nlohmann::json(ImageID)>;
|
||||||
|
using TextureReader = std::function<ImageID(const nlohmann::json&)>;
|
||||||
|
|
||||||
|
[[nodiscard]] nlohmann::json ToJson(const Material& material, const TextureWriter& writeTexture);
|
||||||
|
[[nodiscard]] Material FromJson(const nlohmann::json& data, const TextureReader& readTexture);
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<std::string> Validate(const nlohmann::json& materials);
|
||||||
|
|
||||||
|
// Materials that only exist at runtime (no source file) are stored in the scene or prefab
|
||||||
|
// file under their cache key, so the materialKeys in its objects resolve after a restart
|
||||||
|
[[nodiscard]] nlohmann::json CollectEmbedded(const nlohmann::json& objects);
|
||||||
|
void RegisterEmbedded(const nlohmann::json& materials);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif //DESTRUM_MATERIALSERIALIZATION_H
|
||||||
@@ -201,6 +201,19 @@ std::filesystem::path AssetFS::GetCookedPathForFile(std::string_view vpath) cons
|
|||||||
return rawPath;
|
return rawPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> AssetFS::ToVirtualPath(const std::filesystem::path& fullPath) const {
|
||||||
|
const std::filesystem::path normalized = fullPath.lexically_normal();
|
||||||
|
for (const FSMount& mount : mounts) {
|
||||||
|
const std::filesystem::path relative = normalized.lexically_relative(mount.root);
|
||||||
|
if (relative.empty() || relative.is_absolute() || relative == "." ||
|
||||||
|
relative == ".." || relative.generic_string().starts_with("../")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return mount.scheme + "://" + relative.generic_string();
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> AssetFS::ReadFile(const std::filesystem::path& fullPath) {
|
std::vector<uint8_t> AssetFS::ReadFile(const std::filesystem::path& fullPath) {
|
||||||
std::ifstream file(fullPath, std::ios::binary);
|
std::ifstream file(fullPath, std::ios::binary);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ ImageID ImageCache::addImage(ImageID id, GPUImage image) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ImageCache::LoadedImageInfo* ImageCache::findLoadedImageInfo(ImageID id) const {
|
||||||
|
const auto it = loadedImagesInfo.find(id);
|
||||||
|
return it != loadedImagesInfo.end() ? &it->second : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void ImageCache::removeImage(ImageID id) {
|
void ImageCache::removeImage(ImageID id) {
|
||||||
if (id >= images.size() || images[id].image == VK_NULL_HANDLE) {
|
if (id >= images.size() || images[id].image == VK_NULL_HANDLE) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ void MaterialCache::cleanup(GfxDevice& gfxDevice)
|
|||||||
device = nullptr;
|
device = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialID MaterialCache::addMaterial(Material material)
|
MaterialID MaterialCache::addMaterial(Material material, std::string key)
|
||||||
{
|
{
|
||||||
MaterialData* data = static_cast<MaterialData*>(materialDataBuffer.info.pMappedData);
|
MaterialData* data = static_cast<MaterialData*>(materialDataBuffer.info.pMappedData);
|
||||||
|
|
||||||
@@ -71,10 +71,9 @@ MaterialID MaterialCache::addMaterial(Material material)
|
|||||||
sizeof(MaterialData));
|
sizeof(MaterialData));
|
||||||
|
|
||||||
const auto materialId = static_cast<MaterialID>(materials.size());
|
const auto materialId = static_cast<MaterialID>(materials.size());
|
||||||
std::string key;
|
if (key.empty() && !material.assetReference.empty()) {
|
||||||
if (!material.assetReference.empty()) {
|
|
||||||
key = material.assetReference.cacheKey();
|
key = material.assetReference.cacheKey();
|
||||||
} else {
|
} else if (key.empty()) {
|
||||||
key = material.name.empty()
|
key = material.name.empty()
|
||||||
? "material:" + std::to_string(materialId)
|
? "material:" + std::to_string(materialId)
|
||||||
: material.name;
|
: material.name;
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
#include <destrum/Serialization/MaterialSerialization.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include <destrum/FS/AssetFS.h>
|
||||||
|
#include <destrum/Graphics/GfxDevice.h>
|
||||||
|
#include <destrum/Graphics/Renderer.h>
|
||||||
|
#include <destrum/Serialization/GlmJson.h>
|
||||||
|
#include <destrum/Util/GameState.h>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
using TextureSlot = std::pair<const char*, ImageID Material::*>;
|
||||||
|
|
||||||
|
constexpr std::array<TextureSlot, 6> TextureSlots{{
|
||||||
|
{"diffuse", &Material::diffuseTexture},
|
||||||
|
{"normal", &Material::normalTexture},
|
||||||
|
{"metallic", &Material::metallicTexture},
|
||||||
|
{"roughness", &Material::roughnessTexture},
|
||||||
|
{"occlusion", &Material::occlusionTexture},
|
||||||
|
{"emissive", &Material::emissiveTexture},
|
||||||
|
}};
|
||||||
|
|
||||||
|
[[nodiscard]] const char* FilteringName(TextureFilteringMode mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case TextureFilteringMode::Nearest: return "nearest";
|
||||||
|
case TextureFilteringMode::Anisotropic: return "anisotropic";
|
||||||
|
case TextureFilteringMode::Linear: break;
|
||||||
|
}
|
||||||
|
return "linear";
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] TextureFilteringMode ParseFiltering(const std::string& name) {
|
||||||
|
if (name == "nearest") return TextureFilteringMode::Nearest;
|
||||||
|
if (name == "anisotropic") return TextureFilteringMode::Anisotropic;
|
||||||
|
return TextureFilteringMode::Linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] RenderResources* GetResources() {
|
||||||
|
if (!GameState::GetInstance().HasRenderer() || !GameState::GetInstance().HasGfxDevice()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return GameState::GetInstance().Renderer().getResources();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace MaterialSerialization {
|
||||||
|
json ToJson(const Material& material, const TextureWriter& writeTexture) {
|
||||||
|
json data = {
|
||||||
|
{"name", material.name},
|
||||||
|
{"baseColor", material.baseColor},
|
||||||
|
{"metallic", material.metallicFactor},
|
||||||
|
{"roughness", material.roughnessFactor},
|
||||||
|
{"emissiveColor", material.emissiveColor},
|
||||||
|
{"normalScale", material.normalScale},
|
||||||
|
{"occlusionStrength", material.occlusionStrength},
|
||||||
|
{"filtering", FilteringName(material.textureFilteringMode)},
|
||||||
|
};
|
||||||
|
|
||||||
|
json textures = json::object();
|
||||||
|
for (const auto& [slot, member] : TextureSlots) {
|
||||||
|
if (material.*member == NULL_IMAGE_ID) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
json texture = writeTexture(material.*member);
|
||||||
|
if (!texture.is_null()) {
|
||||||
|
textures[slot] = std::move(texture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!textures.empty()) {
|
||||||
|
data["textures"] = std::move(textures);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Material FromJson(const json& data, const TextureReader& readTexture) {
|
||||||
|
Material material{};
|
||||||
|
material.name = data.value("name", std::string{});
|
||||||
|
material.baseColor = data.value("baseColor", material.baseColor);
|
||||||
|
material.metallicFactor = data.value("metallic", material.metallicFactor);
|
||||||
|
material.roughnessFactor = data.value("roughness", material.roughnessFactor);
|
||||||
|
material.emissiveColor = data.value("emissiveColor", material.emissiveColor);
|
||||||
|
material.normalScale = data.value("normalScale", material.normalScale);
|
||||||
|
material.occlusionStrength = data.value("occlusionStrength", material.occlusionStrength);
|
||||||
|
material.textureFilteringMode = ParseFiltering(data.value("filtering", std::string{"linear"}));
|
||||||
|
|
||||||
|
if (data.contains("textures")) {
|
||||||
|
const json& textures = data.at("textures");
|
||||||
|
for (const auto& [slot, member] : TextureSlots) {
|
||||||
|
if (textures.contains(slot)) {
|
||||||
|
material.*member = readTexture(textures.at(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return material;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> Validate(const json& materials) {
|
||||||
|
if (!materials.is_object()) {
|
||||||
|
return "\"materials\" must be an object";
|
||||||
|
}
|
||||||
|
for (auto it = materials.begin(); it != materials.end(); ++it) {
|
||||||
|
const std::string context = "materials[\"" + it.key() + "\"]";
|
||||||
|
if (!it.value().is_object()) {
|
||||||
|
return context + " must be an object";
|
||||||
|
}
|
||||||
|
if (!it.value().contains("textures")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const json& textures = it.value().at("textures");
|
||||||
|
if (!textures.is_object()) {
|
||||||
|
return context + ".textures must be an object";
|
||||||
|
}
|
||||||
|
for (auto texture = textures.begin(); texture != textures.end(); ++texture) {
|
||||||
|
if (!texture.value().is_object() || !texture.value().contains("path") ||
|
||||||
|
!texture.value().at("path").is_string()) {
|
||||||
|
return context + ".textures." + texture.key() + " must have a \"path\" string";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
json CollectEmbedded(const json& objects) {
|
||||||
|
json materials = json::object();
|
||||||
|
const RenderResources* resources = GetResources();
|
||||||
|
if (resources == nullptr) {
|
||||||
|
return materials;
|
||||||
|
}
|
||||||
|
const MaterialCache& cache = resources->materials();
|
||||||
|
|
||||||
|
const auto writeTexture = [&](ImageID id) -> json {
|
||||||
|
const ImageCache::LoadedImageInfo* info = resources->images().findLoadedImageInfo(id);
|
||||||
|
if (info == nullptr) {
|
||||||
|
spdlog::warn("Texture {} was not loaded from a file, it is not saved", id);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const auto virtualPath = AssetFS::GetInstance().ToVirtualPath(info->path);
|
||||||
|
return {
|
||||||
|
{"path", virtualPath ? *virtualPath : info->path.generic_string()},
|
||||||
|
{"srgb", info->intent == TextureIntent::ColorSrgb},
|
||||||
|
{"mipMap", info->mipMap},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const json& objectJson : objects) {
|
||||||
|
if (!objectJson.contains("components")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const json& componentJson : objectJson.at("components")) {
|
||||||
|
if (!componentJson.contains("data") || !componentJson.at("data").contains("materialKey")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const json& keyJson = componentJson.at("data").at("materialKey");
|
||||||
|
if (!keyJson.is_string() || materials.contains(keyJson.get<std::string>())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string key = keyJson.get<std::string>();
|
||||||
|
const auto id = cache.findMaterialByKey(key);
|
||||||
|
if (!id || *id == cache.getPlaceholderMaterialId() ||
|
||||||
|
!cache.getMaterial(*id).assetReference.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
materials[key] = ToJson(cache.getMaterial(*id), writeTexture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return materials;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RegisterEmbedded(const json& materials) {
|
||||||
|
RenderResources* resources = GetResources();
|
||||||
|
if (resources == nullptr || !materials.is_object()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GfxDevice& gfxDevice = GameState::GetInstance().Gfx();
|
||||||
|
|
||||||
|
const auto readTexture = [&](const json& texture) -> ImageID {
|
||||||
|
const std::string path = texture.at("path").get<std::string>();
|
||||||
|
const TextureIntent intent = texture.value("srgb", true)
|
||||||
|
? TextureIntent::ColorSrgb
|
||||||
|
: TextureIntent::DataLinear;
|
||||||
|
try {
|
||||||
|
const std::filesystem::path fullPath = path.find("://") != std::string::npos
|
||||||
|
? AssetFS::GetInstance().GetFullPath(path)
|
||||||
|
: std::filesystem::path{path};
|
||||||
|
return resources->loadImageFromFile(
|
||||||
|
gfxDevice, fullPath, VK_IMAGE_USAGE_SAMPLED_BIT, texture.value("mipMap", true), intent);
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
spdlog::warn("Material texture \"{}\" could not be loaded: {}", path, exception.what());
|
||||||
|
return resources->getErrorTextureID();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// A key that is already cached wins, so duplicating objects keeps sharing their material
|
||||||
|
for (auto it = materials.begin(); it != materials.end(); ++it) {
|
||||||
|
if (resources->materials().findMaterialByKey(it.key())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
resources->materials().addMaterial(FromJson(it.value(), readTexture), it.key());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <destrum/ObjectModel/Transform.h>
|
#include <destrum/ObjectModel/Transform.h>
|
||||||
#include <destrum/Scene/Scene.h>
|
#include <destrum/Scene/Scene.h>
|
||||||
#include <destrum/Serialization/ComponentRegistry.h>
|
#include <destrum/Serialization/ComponentRegistry.h>
|
||||||
|
#include <destrum/Serialization/MaterialSerialization.h>
|
||||||
#include <destrum/Serialization/ObjectSerialization.h>
|
#include <destrum/Serialization/ObjectSerialization.h>
|
||||||
#include <destrum/Serialization/PrefabCache.h>
|
#include <destrum/Serialization/PrefabCache.h>
|
||||||
|
|
||||||
@@ -33,6 +34,9 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (prefab.contains("materials")) {
|
||||||
|
MaterialSerialization::RegisterEmbedded(prefab.at("materials"));
|
||||||
|
}
|
||||||
auto built = ObjectSerialization::BuildObjects(scene, prefab.at("objects"), externalObjects);
|
auto built = ObjectSerialization::BuildObjects(scene, prefab.at("objects"), externalObjects);
|
||||||
GameObject* root = built.objects.front();
|
GameObject* root = built.objects.front();
|
||||||
if (parent != nullptr) {
|
if (parent != nullptr) {
|
||||||
@@ -58,6 +62,9 @@ std::optional<json> PrefabSerializer::Capture(const GameObject& root) {
|
|||||||
prefab["name"] = root.GetName();
|
prefab["name"] = root.GetName();
|
||||||
try {
|
try {
|
||||||
prefab["objects"] = ObjectSerialization::SerializeObjects(ObjectSerialization::CollectHierarchy(root));
|
prefab["objects"] = ObjectSerialization::SerializeObjects(ObjectSerialization::CollectHierarchy(root));
|
||||||
|
if (json materials = MaterialSerialization::CollectEmbedded(prefab["objects"]); !materials.empty()) {
|
||||||
|
prefab["materials"] = std::move(materials);
|
||||||
|
}
|
||||||
} catch (const std::exception& exception) {
|
} catch (const std::exception& exception) {
|
||||||
spdlog::error("Cannot capture prefab \"{}\": {}", root.GetName(), exception.what());
|
spdlog::error("Cannot capture prefab \"{}\": {}", root.GetName(), exception.what());
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
@@ -135,6 +142,12 @@ std::optional<std::string> PrefabSerializer::Validate(const json& prefab) {
|
|||||||
return "prefab must contain a non-empty \"objects\" array";
|
return "prefab must contain a non-empty \"objects\" array";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prefab.contains("materials")) {
|
||||||
|
if (auto error = MaterialSerialization::Validate(prefab.at("materials"))) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const auto& objects = prefab.at("objects");
|
const auto& objects = prefab.at("objects");
|
||||||
if (auto error = ObjectSerialization::ValidateObjects(objects, ObjectSerialization::CurrentObjectVersion)) {
|
if (auto error = ObjectSerialization::ValidateObjects(objects, ObjectSerialization::CurrentObjectVersion)) {
|
||||||
return error;
|
return error;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <destrum/Components/Physics/Rigidbody.h>
|
#include <destrum/Components/Physics/Rigidbody.h>
|
||||||
#include <destrum/Serialization/ComponentRegistry.h>
|
#include <destrum/Serialization/ComponentRegistry.h>
|
||||||
#include <destrum/Serialization/GlmJson.h>
|
#include <destrum/Serialization/GlmJson.h>
|
||||||
|
#include <destrum/Serialization/MaterialSerialization.h>
|
||||||
#include <destrum/Serialization/ObjectSerialization.h>
|
#include <destrum/Serialization/ObjectSerialization.h>
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
@@ -49,6 +50,12 @@ namespace {
|
|||||||
return "\"currentCamera\" must be an object id";
|
return "\"currentCamera\" must be an object id";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (root.contains("materials")) {
|
||||||
|
if (auto error = MaterialSerialization::Validate(root.at("materials"))) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ObjectSerialization::ValidateObjects(root.at("objects"), version);
|
return ObjectSerialization::ValidateObjects(root.at("objects"), version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,6 +101,9 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
root["objects"] = ObjectSerialization::SerializeObjects(objects);
|
root["objects"] = ObjectSerialization::SerializeObjects(objects);
|
||||||
|
if (json materials = MaterialSerialization::CollectEmbedded(root["objects"]); !materials.empty()) {
|
||||||
|
root["materials"] = std::move(materials);
|
||||||
|
}
|
||||||
} catch (const std::exception& exception) {
|
} catch (const std::exception& exception) {
|
||||||
spdlog::error("Cannot save scene \"{}\": {}", scene.GetName(), exception.what());
|
spdlog::error("Cannot save scene \"{}\": {}", scene.GetName(), exception.what());
|
||||||
return false;
|
return false;
|
||||||
@@ -183,6 +193,11 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
|||||||
std::string context = "scene";
|
std::string context = "scene";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (root.contains("materials")) {
|
||||||
|
context = "materials";
|
||||||
|
MaterialSerialization::RegisterEmbedded(root.at("materials"));
|
||||||
|
context = "scene";
|
||||||
|
}
|
||||||
idMap = ObjectSerialization::BuildObjects(stagingScene, root.at("objects")).idMap;
|
idMap = ObjectSerialization::BuildObjects(stagingScene, root.at("objects")).idMap;
|
||||||
|
|
||||||
// Move objects from pending to objects list without physics registration
|
// Move objects from pending to objects list without physics registration
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Title</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
background-color: hotpink;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <destrum/Scene/Scene.h>
|
#include <destrum/Scene/Scene.h>
|
||||||
#include <destrum/Scene/SceneManager.h>
|
#include <destrum/Scene/SceneManager.h>
|
||||||
#include <destrum/Serialization/GlmJson.h>
|
#include <destrum/Serialization/GlmJson.h>
|
||||||
|
#include <destrum/Serialization/MaterialSerialization.h>
|
||||||
#include <destrum/Serialization/SceneSerializer.h>
|
#include <destrum/Serialization/SceneSerializer.h>
|
||||||
#include <destrum/Components/CameraComponent.h>
|
#include <destrum/Components/CameraComponent.h>
|
||||||
#include <destrum/Components/LightComponent.h>
|
#include <destrum/Components/LightComponent.h>
|
||||||
@@ -281,6 +282,58 @@ namespace {
|
|||||||
|
|
||||||
SceneManager::GetInstance().Destroy();
|
SceneManager::GetInstance().Destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void testMaterialJsonRoundTrip()
|
||||||
|
{
|
||||||
|
Material material = Material::SimpleColor(glm::vec3{0.2f, 0.4f, 0.6f}, "Crate");
|
||||||
|
material.metallicFactor = 0.3f;
|
||||||
|
material.roughnessFactor = 0.55f;
|
||||||
|
material.emissiveColor = {1.0f, 0.5f, 0.0f};
|
||||||
|
material.textureFilteringMode = TextureFilteringMode::Anisotropic;
|
||||||
|
material.diffuseTexture = 3;
|
||||||
|
material.emissiveTexture = 7;
|
||||||
|
material.normalTexture = 9;
|
||||||
|
|
||||||
|
// Texture 9 has no source file, it must be left out instead of saved as a dangling id
|
||||||
|
const nlohmann::json data = MaterialSerialization::ToJson(material, [](ImageID id) -> nlohmann::json {
|
||||||
|
if (id == 9) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return {{"path", "game://tex" + std::to_string(id) + ".png"}, {"srgb", id == 3}};
|
||||||
|
});
|
||||||
|
check(!MaterialSerialization::Validate(nlohmann::json{{"Crate", data}}).has_value(),
|
||||||
|
"a serialized material must pass validation");
|
||||||
|
check(!data.at("textures").contains("normal"), "textures without a source must not be saved");
|
||||||
|
|
||||||
|
const Material loaded = MaterialSerialization::FromJson(data, [](const nlohmann::json& texture) {
|
||||||
|
const std::string path = texture.at("path").get<std::string>();
|
||||||
|
return path == "game://tex3.png" ? ImageID{30} : ImageID{70};
|
||||||
|
});
|
||||||
|
check(loaded.name == "Crate" && loaded.baseColor == material.baseColor &&
|
||||||
|
close(loaded.metallicFactor, 0.3f) && close(loaded.roughnessFactor, 0.55f) &&
|
||||||
|
loaded.emissiveColor == material.emissiveColor &&
|
||||||
|
loaded.textureFilteringMode == TextureFilteringMode::Anisotropic,
|
||||||
|
"material factors must round-trip through json");
|
||||||
|
check(loaded.diffuseTexture == 30 && loaded.emissiveTexture == 70 &&
|
||||||
|
loaded.normalTexture == NULL_IMAGE_ID && loaded.metallicTexture == NULL_IMAGE_ID,
|
||||||
|
"material textures must round-trip through their slots");
|
||||||
|
}
|
||||||
|
|
||||||
|
void testSceneValidatesEmbeddedMaterials()
|
||||||
|
{
|
||||||
|
const auto path = std::filesystem::temp_directory_path() / "destrum_materials_scene.json";
|
||||||
|
Scene& scene = SceneManager::GetInstance().CreateScene("materials");
|
||||||
|
|
||||||
|
std::ofstream{path} << R"({"version":2,"objects":[],"materials":{"Bad":{"textures":{"diffuse":5}}}})";
|
||||||
|
check(!SceneSerializer::Load(scene, path), "a material texture without a path must be rejected");
|
||||||
|
|
||||||
|
std::ofstream{path} << R"({"version":2,"objects":[{"id":1,"name":"box"}],)"
|
||||||
|
R"("materials":{"Good":{"baseColor":[1,0,0],"textures":{"diffuse":{"path":"game://a.png"}}}}})";
|
||||||
|
check(SceneSerializer::Load(scene, path), "a scene with embedded materials must load");
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
|
||||||
|
SceneManager::GetInstance().Destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RunSerializationTests()
|
void RunSerializationTests()
|
||||||
@@ -295,4 +348,6 @@ void RunSerializationTests()
|
|||||||
testSceneLoadDropsMissingCamera();
|
testSceneLoadDropsMissingCamera();
|
||||||
testGlmJson();
|
testGlmJson();
|
||||||
testSceneFileIdsAreLocal();
|
testSceneFileIdsAreLocal();
|
||||||
|
testMaterialJsonRoundTrip();
|
||||||
|
testSceneValidatesEmbeddedMaterials();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user