fix: material serialization
This commit is contained in:
@@ -119,6 +119,7 @@ set(SRC_FILES
|
||||
"src/Serialization/SceneSerializer.cpp"
|
||||
"src/Serialization/ObjectSerialization.cpp"
|
||||
"src/Serialization/PrefabSerializer.cpp"
|
||||
"src/Serialization/MaterialSerialization.cpp"
|
||||
"src/Serialization/PrefabCache.cpp"
|
||||
"src/Serialization/ComponentRegistry.cpp"
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ public:
|
||||
|
||||
[[nodiscard]] std::filesystem::path GetCookedPathForFile(std::string_view vpath) const;
|
||||
|
||||
[[nodiscard]] std::optional<std::string> ToVirtualPath(const std::filesystem::path& fullPath) const;
|
||||
|
||||
private:
|
||||
static std::vector<uint8_t> ReadFile(const std::filesystem::path& fullPath);
|
||||
|
||||
|
||||
@@ -19,6 +19,13 @@ class ImageCache {
|
||||
friend class ResourcesInspector;
|
||||
|
||||
public:
|
||||
struct LoadedImageInfo {
|
||||
std::filesystem::path path;
|
||||
TextureIntent intent;
|
||||
VkImageUsageFlags usage;
|
||||
bool mipMap;
|
||||
};
|
||||
|
||||
explicit ImageCache(GfxDevice& gfxDevice);
|
||||
|
||||
ImageID loadImageFromFile(
|
||||
@@ -29,6 +36,7 @@ public:
|
||||
ImageID addImage(GPUImage image);
|
||||
ImageID addImage(ImageID id, GPUImage image);
|
||||
[[nodiscard]] const GPUImage& getImage(ImageID id) const;
|
||||
[[nodiscard]] const LoadedImageInfo* findLoadedImageInfo(ImageID id) const;
|
||||
void removeImage(ImageID id);
|
||||
|
||||
[[nodiscard]] ImageID getFreeImageId() const;
|
||||
@@ -57,12 +65,6 @@ private:
|
||||
std::shared_ptr<std::vector<ImageID>> freeIds = std::make_shared<std::vector<ImageID>>();
|
||||
GfxDevice& gfxDevice;
|
||||
|
||||
struct LoadedImageInfo {
|
||||
std::filesystem::path path;
|
||||
TextureIntent intent;
|
||||
VkImageUsageFlags usage;
|
||||
bool mipMap;
|
||||
};
|
||||
std::unordered_map<ImageID, LoadedImageInfo> loadedImagesInfo;
|
||||
ImageID errorImageId{NULL_IMAGE_ID};
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ public:
|
||||
|
||||
void cleanup(GfxDevice& gfxDevice);
|
||||
|
||||
MaterialID addMaterial(Material material);
|
||||
MaterialID addMaterial(Material material, std::string key = {});
|
||||
MaterialID addSimpleColorMaterial(
|
||||
glm::vec3 color,
|
||||
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;
|
||||
}
|
||||
|
||||
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::ifstream file(fullPath, std::ios::binary);
|
||||
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) {
|
||||
if (id >= images.size() || images[id].image == VK_NULL_HANDLE) {
|
||||
return;
|
||||
|
||||
@@ -50,7 +50,7 @@ void MaterialCache::cleanup(GfxDevice& gfxDevice)
|
||||
device = nullptr;
|
||||
}
|
||||
|
||||
MaterialID MaterialCache::addMaterial(Material material)
|
||||
MaterialID MaterialCache::addMaterial(Material material, std::string key)
|
||||
{
|
||||
MaterialData* data = static_cast<MaterialData*>(materialDataBuffer.info.pMappedData);
|
||||
|
||||
@@ -71,10 +71,9 @@ MaterialID MaterialCache::addMaterial(Material material)
|
||||
sizeof(MaterialData));
|
||||
|
||||
const auto materialId = static_cast<MaterialID>(materials.size());
|
||||
std::string key;
|
||||
if (!material.assetReference.empty()) {
|
||||
if (key.empty() && !material.assetReference.empty()) {
|
||||
key = material.assetReference.cacheKey();
|
||||
} else {
|
||||
} else if (key.empty()) {
|
||||
key = material.name.empty()
|
||||
? "material:" + std::to_string(materialId)
|
||||
: 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/Scene/Scene.h>
|
||||
#include <destrum/Serialization/ComponentRegistry.h>
|
||||
#include <destrum/Serialization/MaterialSerialization.h>
|
||||
#include <destrum/Serialization/ObjectSerialization.h>
|
||||
#include <destrum/Serialization/PrefabCache.h>
|
||||
|
||||
@@ -33,6 +34,9 @@ namespace {
|
||||
}
|
||||
|
||||
try {
|
||||
if (prefab.contains("materials")) {
|
||||
MaterialSerialization::RegisterEmbedded(prefab.at("materials"));
|
||||
}
|
||||
auto built = ObjectSerialization::BuildObjects(scene, prefab.at("objects"), externalObjects);
|
||||
GameObject* root = built.objects.front();
|
||||
if (parent != nullptr) {
|
||||
@@ -58,6 +62,9 @@ std::optional<json> PrefabSerializer::Capture(const GameObject& root) {
|
||||
prefab["name"] = root.GetName();
|
||||
try {
|
||||
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) {
|
||||
spdlog::error("Cannot capture prefab \"{}\": {}", root.GetName(), exception.what());
|
||||
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";
|
||||
}
|
||||
|
||||
if (prefab.contains("materials")) {
|
||||
if (auto error = MaterialSerialization::Validate(prefab.at("materials"))) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
const auto& objects = prefab.at("objects");
|
||||
if (auto error = ObjectSerialization::ValidateObjects(objects, ObjectSerialization::CurrentObjectVersion)) {
|
||||
return error;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <destrum/Components/Physics/Rigidbody.h>
|
||||
#include <destrum/Serialization/ComponentRegistry.h>
|
||||
#include <destrum/Serialization/GlmJson.h>
|
||||
#include <destrum/Serialization/MaterialSerialization.h>
|
||||
#include <destrum/Serialization/ObjectSerialization.h>
|
||||
|
||||
using json = nlohmann::json;
|
||||
@@ -49,6 +50,12 @@ namespace {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -94,6 +101,9 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
||||
|
||||
try {
|
||||
root["objects"] = ObjectSerialization::SerializeObjects(objects);
|
||||
if (json materials = MaterialSerialization::CollectEmbedded(root["objects"]); !materials.empty()) {
|
||||
root["materials"] = std::move(materials);
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::error("Cannot save scene \"{}\": {}", scene.GetName(), exception.what());
|
||||
return false;
|
||||
@@ -183,6 +193,11 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
||||
std::string context = "scene";
|
||||
|
||||
try {
|
||||
if (root.contains("materials")) {
|
||||
context = "materials";
|
||||
MaterialSerialization::RegisterEmbedded(root.at("materials"));
|
||||
context = "scene";
|
||||
}
|
||||
idMap = ObjectSerialization::BuildObjects(stagingScene, root.at("objects")).idMap;
|
||||
|
||||
// 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/SceneManager.h>
|
||||
#include <destrum/Serialization/GlmJson.h>
|
||||
#include <destrum/Serialization/MaterialSerialization.h>
|
||||
#include <destrum/Serialization/SceneSerializer.h>
|
||||
#include <destrum/Components/CameraComponent.h>
|
||||
#include <destrum/Components/LightComponent.h>
|
||||
@@ -281,6 +282,58 @@ namespace {
|
||||
|
||||
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()
|
||||
@@ -295,4 +348,6 @@ void RunSerializationTests()
|
||||
testSceneLoadDropsMissingCamera();
|
||||
testGlmJson();
|
||||
testSceneFileIdsAreLocal();
|
||||
testMaterialJsonRoundTrip();
|
||||
testSceneValidatesEmbeddedMaterials();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user