Added new component and panel + model loader

This commit is contained in:
Antoine Pilote
2023-08-12 21:47:26 -04:00
parent e0d8383d00
commit 0caf28943f
13 changed files with 712 additions and 4 deletions

View File

@@ -0,0 +1,79 @@
#pragma once
#include <src/Core/Core.h>
#include "ComponentPanel.h"
#include "ModelResourceInspector.h"
#include <src/Scene/Entities/ImGuiHelper.h>
#include <src/Scene/Components/SkinnedModelComponent.h>
#include <src/Resource/ResourceLoader.h>
#include <src/Core/String.h>
class SkinnedModelPanel : ComponentPanel
{
private:
Scope<ModelResourceInspector> _modelInspector;
bool _expanded = false;
public:
SkinnedModelPanel()
{
CreateScope<ModelResourceInspector>();
}
void Draw(Nuake::Entity entity) override
{
using namespace Nuake;
if (!entity.HasComponent<SkinnedModelComponent>())
return;
SkinnedModelComponent& component = entity.GetComponent<SkinnedModelComponent>();
BeginComponentTable(SKINNED MESH, SkinnedModelComponent);
{
ImGui::Text("Model");
ImGui::TableNextColumn();
std::string label = "None";
const bool isModelNone = component.ModelResource == nullptr;
if (!isModelNone)
{
label = std::to_string(component.ModelResource->ID);
}
if (ImGui::Button(label.c_str(), ImVec2(ImGui::GetContentRegionAvail().x, 0)))
{
}
if (_expanded)
{
_modelInspector->Draw();
}
if (ImGui::BeginDragDropTarget())
{
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("_Model"))
{
char* file = (char*)payload->Data;
std::string fullPath = std::string(file, 256);
fullPath = Nuake::FileSystem::AbsoluteToRelative(fullPath);
if (Nuake::String::EndsWith(fullPath, ".model"))
{
}
else
{
component.ModelPath = fullPath;
component.LoadModel();
}
}
ImGui::EndDragDropTarget();
}
ImGui::TableNextColumn();
ComponentTableReset(component.ModelPath, "");
}
EndComponentTable();
}
};

View File

@@ -103,6 +103,7 @@ void EditorSelectionPanel::DrawEntity(Nuake::Entity entity)
mParticleEmitterPanel.Draw(entity);
mSpritePanel.Draw(entity);
mMeshPanel.Draw(entity);
mSkinnedModelPanel.Draw(entity);
mQuakeMapPanel.Draw(entity);
mCameraPanel.Draw(entity);
mRigidbodyPanel.Draw(entity);
@@ -131,6 +132,7 @@ void EditorSelectionPanel::DrawAddComponentMenu(Nuake::Entity entity)
MenuItemComponent("Camera", Nuake::CameraComponent)
MenuItemComponent("Light", Nuake::LightComponent)
MenuItemComponent("Model", Nuake::ModelComponent)
MenuItemComponent("Skinned Model", Nuake::SkinnedModelComponent)
MenuItemComponent("Sprite", Nuake::SpriteComponent)
MenuItemComponent("Particle Emitter", Nuake::ParticleEmitterComponent)
ImGui::Separator();

View File

@@ -20,6 +20,7 @@
#include "../ComponentsPanel/CharacterControllerPanel.h"
#include "../ComponentsPanel/SpritePanel.h"
#include "../ComponentsPanel/ParticleEmitterPanel.h"
#include "../ComponentsPanel/SkinnedModelPanel.h"
class EditorSelectionPanel
@@ -29,6 +30,7 @@ private:
LightPanel mLightPanel;
ScriptPanel mScriptPanel;
MeshPanel mMeshPanel;
SkinnedModelPanel mSkinnedModelPanel;
QuakeMapPanel mQuakeMapPanel;
CameraPanel mCameraPanel;
RigidbodyPanel mRigidbodyPanel;

View File

@@ -8,6 +8,15 @@ namespace Nuake {
Vector3 Min;
Vector3 Max;
AABB() = default;
~AABB() = default;
AABB(const Vector3& min, const Vector3& max)
{
Min = min;
Max = max;
}
// Transforms the bounding box and recalculate an axis aligned box
void Transform(Matrix4 transform)
{

View File

@@ -0,0 +1,197 @@
#include "SkinnedMesh.h"
#include "src/Core/Maths.h"
#include "src/Rendering/Textures/Material.h"
#include "src/Rendering/Textures/MaterialManager.h"
#include "src/Rendering/Renderer.h"
#include "src/Rendering/Shaders/Shader.h"
#include "src/Rendering/Vertex.h"
#include "src/Rendering/Buffers/VertexBuffer.h"
#include "src/Rendering/Buffers/VertexArray.h"
#include "src/Rendering/Buffers/VertexBufferLayout.h"
#include <future>
namespace Nuake
{
void SkinnedMesh::AddSurface(std::vector<SkinnedVertex> vertices, std::vector<uint32_t> indices, std::vector<Bone> bones)
{
m_Vertices = vertices;
m_Indices = indices;
m_Bones = bones;
SetupMesh();
CalculateAABB();
if (m_Material == nullptr)
{
m_Material = MaterialManager::Get()->GetMaterial("default");
}
}
std::vector<SkinnedVertex>& SkinnedMesh::GetVertices()
{
return m_Vertices;
}
std::vector<uint32_t>& SkinnedMesh::GetIndices()
{
return m_Indices;
}
Ref<Material> SkinnedMesh::GetMaterial() inline const
{
return m_Material;
}
void SkinnedMesh::SetMaterial(Ref<Material> material)
{
m_Material = material;
MaterialManager::Get()->RegisterMaterial(material);
}
void SkinnedMesh::CalculateAABB()
{
float minX = 0.0f;
float minY = 0.0f;
float minZ = 0.0f;
float maxX = 0.0f;
float maxY = 0.0f;
float maxZ = 0.0f;
for (const SkinnedVertex& v : m_Vertices)
{
minX = v.position.x < minX ? v.position.x : minX;
minY = v.position.y < minY ? v.position.y : minY;
minZ = v.position.z < minZ ? v.position.z : minZ;
maxX = v.position.x > maxX ? v.position.x : maxX;
maxY = v.position.y > maxY ? v.position.y : maxY;
maxZ = v.position.z > maxZ ? v.position.z : maxZ;
}
m_AABB = AABB(Vector3(minX, minY, minZ), Vector3(maxX, maxY, maxZ));
}
void SkinnedMesh::SetupMesh()
{
m_VertexArray = CreateScope<VertexArray>();
m_VertexArray->Bind();
m_VertexBuffer = CreateScope<VertexBuffer>(m_Vertices.data(), m_Vertices.size() * sizeof(Vertex));
m_ElementBuffer = CreateScope<VertexBuffer>(m_Indices.data(), m_Indices.size() * sizeof(unsigned int), RendererEnum::ELEMENT_ARRAY_BUFFER);
VertexBufferLayout bufferLayout = VertexBufferLayout();
bufferLayout.Push<float>(3); // Position
bufferLayout.Push<float>(2); // UV
bufferLayout.Push<float>(3); // Normal
bufferLayout.Push<float>(3); // Tangent
bufferLayout.Push<float>(3); // Bitangent
bufferLayout.Push<unsigned int>(4); // BoneIds
bufferLayout.Push<float>(4); // Weights
m_VertexArray->AddBuffer(*m_VertexBuffer, bufferLayout);
m_VertexArray->Unbind();
}
void SkinnedMesh::Bind() const
{
m_VertexArray->Bind();
}
void SkinnedMesh::Draw(Shader* shader, bool bindMaterial)
{
if (bindMaterial)
m_Material->Bind(shader);
m_VertexArray->Bind();
RenderCommand::DrawElements(RendererEnum::TRIANGLES, (int)m_Indices.size(), RendererEnum::UINT, 0);
}
void SkinnedMesh::DebugDraw()
{
Renderer::m_DebugShader->Bind();
Renderer::m_DebugShader->SetUniform4f("u_Color", 1.0f, 0.0f, 0.0f, 1.f);
m_VertexArray->Bind();
RenderCommand::DrawElements(RendererEnum::TRIANGLES, (int)m_Indices.size(), RendererEnum::UINT, 0);
}
json SkinnedMesh::Serialize()
{
BEGIN_SERIALIZE();
j["Material"] = m_Material->Serialize();
j["Indices"] = m_Indices;
for (uint32_t i = 0; i < m_Bones.size(); i++)
{
//j["Bones"][i] = m_Bones[i];
}
json v;
for (uint32_t i = 0; i < m_Vertices.size(); i++)
{
v["Position"]["x"] = m_Vertices[i].position.x;
v["Position"]["y"] = m_Vertices[i].position.y;
v["Position"]["z"] = m_Vertices[i].position.z;
v["UV"]["x"] = m_Vertices[i].uv.x;
v["UV"]["y"] = m_Vertices[i].uv.y;
v["Normal"]["x"] = m_Vertices[i].normal.x;
v["Normal"]["y"] = m_Vertices[i].normal.y;
v["Normal"]["z"] = m_Vertices[i].normal.z;
v["Tangent"]["x"] = m_Vertices[i].tangent.x;
v["Tangent"]["y"] = m_Vertices[i].tangent.y;
v["Tangent"]["z"] = m_Vertices[i].tangent.z;
v["Bitangent"]["x"] = m_Vertices[i].bitangent.x;
v["Bitangent"]["y"] = m_Vertices[i].bitangent.y;
v["Bitangent"]["z"] = m_Vertices[i].bitangent.z;
j["Vertices"][i] = v;
}
END_SERIALIZE();
}
bool SkinnedMesh::Deserialize(const json& j)
{
m_Material = CreateRef<Material>();
m_Material->Deserialize(j["Material"]);
m_Indices.reserve(j["Indices"].size());
for (auto& i : j["Indices"])
{
m_Indices.push_back(i);
}
std::vector<SkinnedVertex> vertices;
std::async(std::launch::async, [&]()
{
for (auto& v : j["Vertices"])
{
SkinnedVertex vertex;
try {
DESERIALIZE_VEC2(v["UV"], vertex.uv)
}
catch (std::exception& e) {
vertex.uv = { 0.0, 0.0 };
}
DESERIALIZE_VEC3(v["Position"], vertex.position)
DESERIALIZE_VEC3(v["Normal"], vertex.normal)
DESERIALIZE_VEC3(v["Tangent"], vertex.tangent)
DESERIALIZE_VEC3(v["Bitangent"], vertex.bitangent)
vertices.push_back(vertex);
}
}
);
m_Vertices = vertices;
SetupMesh();
return true;
}
}

View File

@@ -0,0 +1,67 @@
#pragma once
#include "src/Core/Core.h"
#include "src/Rendering/AABB.h"
#include "src/Resource/Resource.h"
#include "src/Resource/Serializable.h"
#include "src/Rendering/Vertex.h"
namespace Nuake
{
class VertexBuffer;
class VertexArray;
class Material;
struct Vertex;
class Shader;
struct BoneVertexWeight
{
uint32_t VertexID;
float Weight;
};
struct Bone
{
std::string Name;
Matrix4 Offset;
std::vector<BoneVertexWeight> VertexWeights;
};
class SkinnedMesh : ISerializable, Resource
{
public:
SkinnedMesh() = default;
~SkinnedMesh() = default;
void AddSurface(std::vector<SkinnedVertex> vertices, std::vector<uint32_t> indices, std::vector<Bone> bones);
std::vector<SkinnedVertex>& GetVertices();
std::vector<uint32_t>& GetIndices();
std::vector<Bone>& GetBones();
Ref<Material> GetMaterial() inline const;
void SetMaterial(Ref<Material> material);
void Bind() const;
void Draw(Shader* shader, bool bindMaterial = true);
void DebugDraw();
inline AABB GetAABB() const { return m_AABB; }
json Serialize() override;
bool Deserialize(const json& j) override;
private:
Ref<Material> m_Material = nullptr;
std::vector<uint32_t> m_Indices;
std::vector<SkinnedVertex> m_Vertices;
std::vector<Bone> m_Bones;
Scope<VertexBuffer> m_VertexBuffer;
Scope<VertexArray> m_VertexArray;
Scope<VertexBuffer> m_ElementBuffer;
void SetupMesh();
AABB m_AABB;
void CalculateAABB();
};
}

View File

@@ -12,6 +12,20 @@ namespace Nuake
Vector3 bitangent;
};
const uint32_t MAX_BONE_INFLUENCE = 4;
struct SkinnedVertex
{
Vector3 position;
Vector2 uv;
Vector3 normal;
Vector3 tangent;
Vector3 bitangent;
int boneIDs[MAX_BONE_INFLUENCE];
//weights from each bone
float weights[MAX_BONE_INFLUENCE];
};
struct LineVertex
{
Vector3 position;

View File

@@ -5,6 +5,7 @@
#include "src/Core/String.h"
#include "src/Resource/SkinnedModel.h"
#include "src/Resource/Model.h"
@@ -18,8 +19,8 @@ namespace Nuake
m_Meshes.clear();
Ref<Model> model = CreateRef<Model>(path);
Assimp::Importer import;
import.SetPropertyFloat("PP_GSN_MAX_SMOOTHING_ANGLE", 90);
Assimp::Importer importer;
importer.SetPropertyFloat("PP_GSN_MAX_SMOOTHING_ANGLE", 90);
auto importFlags =
aiProcess_Triangulate |
@@ -29,10 +30,10 @@ namespace Nuake
modelDir = absolute ? path + "/../" : FileSystem::Root + path + "/../";
const std::string filePath = absolute ? path : FileSystem::Root + path;
const aiScene* scene = import.ReadFile(filePath, importFlags);
const aiScene* scene = importer.ReadFile(filePath, importFlags);
if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
{
std::string assimpErrorMsg = std::string(import.GetErrorString());
std::string assimpErrorMsg = std::string(importer.GetErrorString());
std::string logMsg = "[Failed to load model] - " + assimpErrorMsg;
Logger::Log(logMsg, "model", WARNING);
@@ -49,6 +50,42 @@ namespace Nuake
return model;
}
Ref<SkinnedModel> ModelLoader::LoadSkinnedModel(const std::string& path, bool absolute)
{
m_Meshes.clear();
Ref<SkinnedModel> model = CreateRef<SkinnedModel>(path);
Assimp::Importer importer;
importer.SetPropertyFloat("PP_GSN_MAX_SMOOTHING_ANGLE", 90);
auto importFlags =
aiProcess_Triangulate |
aiProcess_GenSmoothNormals |
aiProcess_FixInfacingNormals |
aiProcess_CalcTangentSpace;
modelDir = absolute ? path + "/../" : FileSystem::Root + path + "/../";
const std::string filePath = absolute ? path : FileSystem::Root + path;
const aiScene* scene = importer.ReadFile(filePath, importFlags);
if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
{
std::string assimpErrorMsg = std::string(importer.GetErrorString());
std::string logMsg = "[Failed to load model] - " + assimpErrorMsg;
Logger::Log(logMsg, "model", WARNING);
return model;
}
ProcessNode(scene->mRootNode, scene);
for (const auto& mesh : m_SkinnedMeshes)
{
model->AddMesh(mesh);
}
return model;
}
void ModelLoader::ProcessNode(aiNode* node, const aiScene* scene)
{
for (uint32_t i = 0; i < node->mNumMeshes; i++)
@@ -63,12 +100,84 @@ namespace Nuake
}
}
void ModelLoader::ProcessSkinnedNode(aiNode* node, const aiScene* scene)
{
for (uint32_t i = 0; i < node->mNumMeshes; i++)
{
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
m_SkinnedMeshes.push_back(ProcessSkinnedMesh(mesh, scene));
}
for (uint32_t i = 0; i < node->mNumChildren; i++)
{
ProcessSkinnedNode(node->mChildren[i], scene);
}
}
Ref<SkinnedMesh> ModelLoader::ProcessSkinnedMesh(aiMesh* node, const aiScene* scene)
{
auto vertices = ProcessSkinnedVertices(node);
auto indices = ProcessIndices(node);
auto material = ProcessMaterials(scene, node);
auto bones = std::vector<Bone>();
if (node->HasBones())
{
for (uint32_t i = 0; i < node->mNumBones; i++)
{
aiBone* bone = node->mBones[i];
const std::string& boneName = bone->mName.C_Str();
const auto& boneMatrix = bone->mOffsetMatrix;
for (uint32_t j = 0; j < bone->mNumWeights; j++)
{
aiVertexWeight vertexWeight = bone->mWeights[j];
const float weigth = vertexWeight.mWeight;
uint32_t vertexId = vertexWeight.mVertexId;
vertices[vertexId].boneIDs[j] = i;
}
}
}
else
{
Logger::Log("Using skinned mesh imported while the model has no bones!", "model loader", WARNING);
}
// Fill in the bones in the vertices
Ref<SkinnedMesh> mesh = CreateRef<SkinnedMesh>();
mesh->AddSurface(vertices, indices, bones);
mesh->SetMaterial(material);
return mesh;
}
Ref<Mesh> ModelLoader::ProcessMesh(aiMesh* node, const aiScene* scene)
{
auto vertices = ProcessVertices(node);
auto indices = ProcessIndices(node);
auto material = ProcessMaterials(scene, node);
if (node->HasBones())
{
for (uint32_t i = 0; i < node->mNumBones; i++)
{
aiBone* bone = node->mBones[i];
const std::string& boneName = bone->mName.C_Str();
const auto& boneMatrix = bone->mOffsetMatrix;
for (uint32_t j = 0; j < bone->mNumWeights; j++)
{
aiVertexWeight vertexWeight = bone->mWeights[j];
const float weigth = vertexWeight.mWeight;
uint32_t vertexId = vertexWeight.mVertexId;
}
}
}
Ref<Mesh> mesh = CreateRef<Mesh>();
mesh->AddSurface(vertices, indices);
mesh->SetMaterial(material);
@@ -130,6 +239,62 @@ namespace Nuake
return vertices;
}
std::vector<SkinnedVertex> ModelLoader::ProcessSkinnedVertices(aiMesh* mesh)
{
auto vertices = std::vector<SkinnedVertex>();
for (uint32_t i = 0; i < mesh->mNumVertices; i++)
{
SkinnedVertex vertex;
Vector3 current;
// Position
current.x = mesh->mVertices[i].x;
current.y = mesh->mVertices[i].y;
current.z = mesh->mVertices[i].z;
vertex.position = current;
// Normals
current.x = mesh->mNormals[i].x;
current.y = mesh->mNormals[i].y;
current.z = mesh->mNormals[i].z;
vertex.normal = current;
// Tangents
if (mesh->mTangents)
{
current.x = mesh->mTangents[i].x;
current.y = mesh->mTangents[i].y;
current.z = mesh->mTangents[i].z;
vertex.tangent = current;
}
if (mesh->mBitangents)
{
current.x = mesh->mBitangents[i].x;
current.y = mesh->mBitangents[i].y;
current.z = mesh->mBitangents[i].z;
vertex.bitangent = current;
}
vertex.uv = glm::vec2(0.0f, 0.0f);
// Does it contain UVs?
if (mesh->mTextureCoords[0])
{
float u = mesh->mTextureCoords[0][i].x;
float v = mesh->mTextureCoords[0][i].y;
vertex.uv = Vector2(u, v);
}
// We are filling the bones later.
vertices.push_back(vertex);
}
return vertices;
}
std::vector<uint32_t> ModelLoader::ProcessIndices(aiMesh* mesh)
{
auto indices = std::vector<uint32_t>();

View File

@@ -1,6 +1,7 @@
#pragma once
#include "src/Core/Core.h"
#include "src/Rendering/Mesh/SkinnedMesh.h"
#include "src/Rendering/Mesh/Mesh.h"
#include "src/Rendering/Textures/Material.h"
#include "src/Rendering/Textures/Texture.h"
@@ -11,9 +12,13 @@
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include "src/Rendering/Buffers/VertexArray.h"
namespace Nuake
{
class Model;
class SkinnedModel;
class ModelLoader
{
public:
@@ -21,9 +26,12 @@ namespace Nuake
~ModelLoader();
Ref<Model> LoadModel(const std::string& path, bool absolute = false);
Ref<SkinnedModel> LoadSkinnedModel(const std::string& path, bool absolute = false);
private:
std::string modelDir;
std::vector<Ref<Mesh>> m_Meshes;
std::vector<Ref<SkinnedMesh>> m_SkinnedMeshes;
void ProcessNode(aiNode* node, const aiScene* scene);
Ref<Mesh> ProcessMesh(aiMesh* node, const aiScene* scene);
@@ -32,5 +40,10 @@ namespace Nuake
std::vector<uint32_t> ProcessIndices(aiMesh* mesh);
Ref<Material> ProcessMaterials(const aiScene* scene, aiMesh* mesh);
Ref<Texture> ProcessTextures(const aiScene* scene, const std::string& path);
// Skinned
void ProcessSkinnedNode(aiNode* node, const aiScene* scene);
Ref<SkinnedMesh> ProcessSkinnedMesh(aiMesh* node, const aiScene* scene);
std::vector<SkinnedVertex> ProcessSkinnedVertices(aiMesh* mesh);
};
}

View File

@@ -0,0 +1,73 @@
#include "src/Resource/ModelLoader.h"
#include "src/Resource/SkinnedModel.h"
#include "src/Core/FileSystem.h"
#include "src/Core/Logger.h"
namespace Nuake
{
SkinnedModel::SkinnedModel(const std::string path) : m_Meshes(std::vector<Ref<SkinnedMesh>>())
{
this->Path = path;
}
SkinnedModel::SkinnedModel() : m_Meshes(std::vector<Ref<SkinnedMesh>>())
{}
SkinnedModel::~SkinnedModel() {}
void SkinnedModel::AddMesh(Ref<SkinnedMesh> mesh)
{
m_Meshes.push_back(mesh);
}
std::vector<Ref<SkinnedMesh>>& SkinnedModel::GetMeshes()
{
return m_Meshes;
}
json SkinnedModel::Serialize()
{
BEGIN_SERIALIZE();
if (this->Path != "")
{
j["Path"] = this->Path;
}
else
{
for (uint32_t i = 0; i < std::size(m_Meshes); i++)
{
j["Meshes"][i] = m_Meshes[i]->Serialize();
}
}
END_SERIALIZE();
}
bool SkinnedModel::Deserialize(const json& j)
{
if (j.contains("Path"))
{
this->IsEmbedded = true;
ModelLoader loader;
auto otherModel = loader.LoadSkinnedModel(j["Path"], false);
m_Meshes = otherModel->GetMeshes();
this->Path = j["Path"];
}
else
{
for (auto& m : j["Meshes"])
{
auto mesh = CreateRef<SkinnedMesh>();
mesh->Deserialize(m);
m_Meshes.push_back(mesh);
}
}
return true;
}
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include "src/Core/Core.h"
#include "src/Rendering/Mesh/SkinnedMesh.h"
#include "src/Resource/Resource.h"
#include "src/Resource/Serializable.h"
namespace Nuake
{
class SkinnedModel : public Resource, ISerializable
{
private:
std::vector<Ref<SkinnedMesh>> m_Meshes;
public:
SkinnedModel();
SkinnedModel(const std::string path);
~SkinnedModel();
void AddMesh(Ref<SkinnedMesh> mesh);
std::vector<Ref<SkinnedMesh>>& GetMeshes();
json Serialize() override;
bool Deserialize(const json& j) override;
};
}

View File

@@ -0,0 +1,16 @@
#include "SkinnedModelComponent.h"
#include "src/Resource/ModelLoader.h"
namespace Nuake
{
SkinnedModelComponent::SkinnedModelComponent()
{
}
void SkinnedModelComponent::LoadModel()
{
auto loader = ModelLoader();
this->ModelResource = loader.LoadSkinnedModel(ModelPath);
}
}

View File

@@ -0,0 +1,45 @@
#pragma once
#include <vector>
#include "src/Resource/Serializable.h"
#include "src/Resource/SkinnedModel.h"
#include <string>
namespace Nuake
{
struct SkinnedModelComponent
{
Ref<SkinnedModel> ModelResource;
std::string ModelPath;
SkinnedModelComponent();
void LoadModel();
std::string directory;
json Serialize()
{
BEGIN_SERIALIZE();
SERIALIZE_VAL(ModelPath);
SERIALIZE_OBJECT(ModelResource);
END_SERIALIZE();
}
bool Deserialize(const json& j)
{
ModelPath = j["ModelPath"];
ModelResource = CreateRef<SkinnedModel>();
if (j.contains("ModelResource"))
{
auto& res = j["ModelResource"];
ModelResource->Deserialize(res);
}
return true;
}
};
}