fix: material serialization
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user