diff --git a/engine/include/XCEngine/Resources/Material/Material.h b/engine/include/XCEngine/Resources/Material/Material.h index 5f76d7d8..bf6f3dc6 100644 --- a/engine/include/XCEngine/Resources/Material/Material.h +++ b/engine/include/XCEngine/Resources/Material/Material.h @@ -14,6 +14,14 @@ namespace XCEngine { namespace Resources { +enum class MaterialRenderQueue : Core::int32 { + Background = 1000, + Geometry = 2000, + AlphaTest = 2450, + Transparent = 3000, + Overlay = 4000 +}; + enum class MaterialPropertyType { Float, Float2, Float3, Float4, Int, Int2, Int3, Int4, @@ -52,6 +60,20 @@ public: void SetShader(const ResourceHandle& shader); class Shader* GetShader() const { return m_shader.Get(); } + + void SetRenderQueue(Core::int32 renderQueue); + void SetRenderQueue(MaterialRenderQueue renderQueue); + Core::int32 GetRenderQueue() const { return m_renderQueue; } + + void SetShaderPass(const Containers::String& shaderPass); + const Containers::String& GetShaderPass() const { return m_shaderPass; } + + void SetTag(const Containers::String& name, const Containers::String& value); + Containers::String GetTag(const Containers::String& name) const; + bool HasTag(const Containers::String& name) const; + void RemoveTag(const Containers::String& name); + void ClearTags(); + Core::uint32 GetTagCount() const { return static_cast(m_tags.Size()); } void SetFloat(const Containers::String& name, float value); void SetFloat2(const Containers::String& name, const Math::Vector2& value); @@ -72,7 +94,8 @@ public: const Containers::Array& GetConstantBufferData() const { return m_constantBufferData; } void UpdateConstantBuffer(); - + void RecalculateMemorySize(); + bool HasProperty(const Containers::String& name) const; void RemoveProperty(const Containers::String& name); void ClearAllProperties(); @@ -81,6 +104,13 @@ private: void UpdateMemorySize(); ResourceHandle m_shader; + Core::int32 m_renderQueue = static_cast(MaterialRenderQueue::Geometry); + Containers::String m_shaderPass; + struct TagEntry { + Containers::String name; + Containers::String value; + }; + Containers::Array m_tags; Containers::HashMap m_properties; Containers::Array m_constantBufferData; diff --git a/engine/src/Core/Asset/ResourceManager.cpp b/engine/src/Core/Asset/ResourceManager.cpp index dba1d22e..725a6c89 100644 --- a/engine/src/Core/Asset/ResourceManager.cpp +++ b/engine/src/Core/Asset/ResourceManager.cpp @@ -1,10 +1,26 @@ #include #include #include +#include +#include namespace XCEngine { namespace Resources { +namespace { + +template +void RegisterBuiltinLoader(ResourceManager& manager, TLoader& loader) { + if (manager.GetLoader(loader.GetResourceType()) == nullptr) { + manager.RegisterLoader(&loader); + } +} + +MaterialLoader g_materialLoader; +ShaderLoader g_shaderLoader; + +} // namespace + ResourceManager& ResourceManager::Get() { static ResourceManager instance; return instance; @@ -13,6 +29,9 @@ ResourceManager& ResourceManager::Get() { void ResourceManager::Initialize() { m_asyncLoader = Core::MakeUnique(); m_asyncLoader->Initialize(2); + + RegisterBuiltinLoader(*this, g_materialLoader); + RegisterBuiltinLoader(*this, g_shaderLoader); } void ResourceManager::Shutdown() { diff --git a/engine/src/Resources/Material/Material.cpp b/engine/src/Resources/Material/Material.cpp index a3e9f480..53d3b93a 100644 --- a/engine/src/Resources/Material/Material.cpp +++ b/engine/src/Resources/Material/Material.cpp @@ -11,6 +11,9 @@ Material::~Material() = default; void Material::Release() { m_shader.Reset(); + m_renderQueue = static_cast(MaterialRenderQueue::Geometry); + m_shaderPass.Clear(); + m_tags.Clear(); m_properties.Clear(); m_textureBindings.Clear(); m_constantBufferData.Clear(); @@ -23,6 +26,73 @@ void Material::SetShader(const ResourceHandle& shader) { UpdateMemorySize(); } +void Material::SetRenderQueue(Core::int32 renderQueue) { + m_renderQueue = renderQueue; +} + +void Material::SetRenderQueue(MaterialRenderQueue renderQueue) { + SetRenderQueue(static_cast(renderQueue)); +} + +void Material::SetShaderPass(const Containers::String& shaderPass) { + m_shaderPass = shaderPass; + UpdateMemorySize(); +} + +void Material::SetTag(const Containers::String& name, const Containers::String& value) { + for (TagEntry& tag : m_tags) { + if (tag.name == name) { + tag.value = value; + UpdateMemorySize(); + return; + } + } + + TagEntry tag; + tag.name = name; + tag.value = value; + m_tags.PushBack(tag); + UpdateMemorySize(); +} + +Containers::String Material::GetTag(const Containers::String& name) const { + for (const TagEntry& tag : m_tags) { + if (tag.name == name) { + return tag.value; + } + } + + return Containers::String(); +} + +bool Material::HasTag(const Containers::String& name) const { + for (const TagEntry& tag : m_tags) { + if (tag.name == name) { + return true; + } + } + + return false; +} + +void Material::RemoveTag(const Containers::String& name) { + for (size_t tagIndex = 0; tagIndex < m_tags.Size(); ++tagIndex) { + if (m_tags[tagIndex].name == name) { + if (tagIndex != m_tags.Size() - 1) { + m_tags[tagIndex] = std::move(m_tags.Back()); + } + m_tags.PopBack(); + UpdateMemorySize(); + return; + } + } +} + +void Material::ClearTags() { + m_tags.Clear(); + UpdateMemorySize(); +} + void Material::SetFloat(const Containers::String& name, float value) { MaterialProperty prop; prop.name = name; @@ -175,6 +245,10 @@ void Material::UpdateConstantBuffer() { UpdateMemorySize(); } +void Material::RecalculateMemorySize() { + UpdateMemorySize(); +} + bool Material::HasProperty(const Containers::String& name) const { return m_properties.Contains(name); } @@ -193,11 +267,18 @@ void Material::ClearAllProperties() { void Material::UpdateMemorySize() { m_memorySize = m_constantBufferData.Size() + + m_shaderPass.Length() + + m_tags.Size() * sizeof(TagEntry) + m_textureBindings.Size() * sizeof(TextureBinding) + m_properties.Size() * sizeof(MaterialProperty) + m_name.Length() + m_path.Length(); + for (const TagEntry& tag : m_tags) { + m_memorySize += tag.name.Length(); + m_memorySize += tag.value.Length(); + } + for (const auto& binding : m_textureBindings) { m_memorySize += binding.name.Length(); } diff --git a/engine/src/Resources/Material/MaterialLoader.cpp b/engine/src/Resources/Material/MaterialLoader.cpp index b42516e4..adca7e6d 100644 --- a/engine/src/Resources/Material/MaterialLoader.cpp +++ b/engine/src/Resources/Material/MaterialLoader.cpp @@ -1,10 +1,256 @@ #include #include #include +#include + +#include +#include +#include namespace XCEngine { namespace Resources { +namespace { + +std::string ToStdString(const Containers::Array& data) { + return std::string(reinterpret_cast(data.Data()), data.Size()); +} + +size_t SkipWhitespace(const std::string& text, size_t pos) { + while (pos < text.size() && std::isspace(static_cast(text[pos])) != 0) { + ++pos; + } + return pos; +} + +bool HasKey(const std::string& json, const char* key) { + const std::string token = std::string("\"") + key + "\""; + return json.find(token) != std::string::npos; +} + +bool FindValueStart(const std::string& json, const char* key, size_t& valuePos) { + const std::string token = std::string("\"") + key + "\""; + const size_t keyPos = json.find(token); + if (keyPos == std::string::npos) { + return false; + } + + const size_t colonPos = json.find(':', keyPos + token.length()); + if (colonPos == std::string::npos) { + return false; + } + + valuePos = SkipWhitespace(json, colonPos + 1); + return valuePos < json.size(); +} + +bool ParseQuotedString(const std::string& text, size_t quotePos, Containers::String& outValue, size_t* nextPos = nullptr) { + if (quotePos >= text.size() || text[quotePos] != '"') { + return false; + } + + std::string parsed; + ++quotePos; + + while (quotePos < text.size()) { + const char ch = text[quotePos]; + if (ch == '\\') { + if (quotePos + 1 >= text.size()) { + return false; + } + + parsed.push_back(text[quotePos + 1]); + quotePos += 2; + continue; + } + + if (ch == '"') { + outValue = parsed.c_str(); + if (nextPos != nullptr) { + *nextPos = quotePos + 1; + } + return true; + } + + parsed.push_back(ch); + ++quotePos; + } + + return false; +} + +bool TryParseStringValue(const std::string& json, const char* key, Containers::String& outValue) { + size_t valuePos = 0; + if (!FindValueStart(json, key, valuePos)) { + return false; + } + + return ParseQuotedString(json, valuePos, outValue); +} + +bool TryParseIntValue(const std::string& json, const char* key, Core::int32& outValue) { + size_t valuePos = 0; + if (!FindValueStart(json, key, valuePos)) { + return false; + } + + if (valuePos >= json.size() || + (json[valuePos] != '-' && std::isdigit(static_cast(json[valuePos])) == 0)) { + return false; + } + + char* endPtr = nullptr; + const long parsed = std::strtol(json.c_str() + valuePos, &endPtr, 10); + if (endPtr == json.c_str() + valuePos) { + return false; + } + + outValue = static_cast(parsed); + return true; +} + +bool TryExtractObject(const std::string& json, const char* key, std::string& outObject) { + size_t valuePos = 0; + if (!FindValueStart(json, key, valuePos) || valuePos >= json.size() || json[valuePos] != '{') { + return false; + } + + bool inString = false; + bool escaped = false; + int depth = 0; + + for (size_t pos = valuePos; pos < json.size(); ++pos) { + const char ch = json[pos]; + if (escaped) { + escaped = false; + continue; + } + + if (ch == '\\') { + escaped = true; + continue; + } + + if (ch == '"') { + inString = !inString; + continue; + } + + if (inString) { + continue; + } + + if (ch == '{') { + ++depth; + } else if (ch == '}') { + --depth; + if (depth == 0) { + outObject = json.substr(valuePos, pos - valuePos + 1); + return true; + } + } + } + + return false; +} + +bool TryParseRenderQueueName(const Containers::String& queueName, Core::int32& outQueue) { + const Containers::String normalized = queueName.ToLower(); + if (normalized == "background") { + outQueue = static_cast(MaterialRenderQueue::Background); + return true; + } + if (normalized == "geometry" || normalized == "opaque") { + outQueue = static_cast(MaterialRenderQueue::Geometry); + return true; + } + if (normalized == "alphatest" || normalized == "alpha_test") { + outQueue = static_cast(MaterialRenderQueue::AlphaTest); + return true; + } + if (normalized == "transparent") { + outQueue = static_cast(MaterialRenderQueue::Transparent); + return true; + } + if (normalized == "overlay") { + outQueue = static_cast(MaterialRenderQueue::Overlay); + return true; + } + + return false; +} + +bool TryParseTagMap(const std::string& objectText, Material* material) { + if (material == nullptr || objectText.empty() || objectText.front() != '{' || objectText.back() != '}') { + return false; + } + + size_t pos = 1; + while (pos < objectText.size()) { + pos = SkipWhitespace(objectText, pos); + if (pos >= objectText.size()) { + return false; + } + + if (objectText[pos] == '}') { + return true; + } + + Containers::String key; + if (!ParseQuotedString(objectText, pos, key, &pos)) { + return false; + } + + pos = SkipWhitespace(objectText, pos); + if (pos >= objectText.size() || objectText[pos] != ':') { + return false; + } + + pos = SkipWhitespace(objectText, pos + 1); + Containers::String value; + if (!ParseQuotedString(objectText, pos, value, &pos)) { + return false; + } + + material->SetTag(key, value); + + pos = SkipWhitespace(objectText, pos); + if (pos >= objectText.size()) { + return false; + } + + if (objectText[pos] == ',') { + ++pos; + continue; + } + + if (objectText[pos] == '}') { + return true; + } + + return false; + } + + return false; +} + +ResourceHandle LoadShaderHandle(const Containers::String& shaderPath) { + ResourceHandle shader = ResourceManager::Get().Load(shaderPath); + if (shader.IsValid()) { + return shader; + } + + ShaderLoader shaderLoader; + LoadResult shaderResult = shaderLoader.Load(shaderPath); + if (!shaderResult || shaderResult.resource == nullptr) { + return ResourceHandle(); + } + + return ResourceHandle(static_cast(shaderResult.resource)); +} + +} // namespace + MaterialLoader::MaterialLoader() = default; MaterialLoader::~MaterialLoader() = default; @@ -23,41 +269,25 @@ bool MaterialLoader::CanLoad(const Containers::String& path) const { } LoadResult MaterialLoader::Load(const Containers::String& path, const ImportSettings* settings) { + (void)settings; + Containers::Array data = ReadFileData(path); if (data.Empty()) { return LoadResult("Failed to read material file: " + path); } - + Material* material = new Material(); material->m_path = path; material->m_name = path; material->m_guid = ResourceGUID::Generate(path); - - Containers::String jsonStr; - jsonStr.Reserve(data.Size()); - for (size_t i = 0; i < data.Size(); ++i) { - jsonStr += static_cast(data[i]); + + if (!ParseMaterialData(data, material)) { + delete material; + return LoadResult("Failed to parse material file: " + path); } - - size_t shaderPos = jsonStr.Find("\"shader\""); - if (shaderPos != Containers::String::npos) { - size_t colonPos = jsonStr.Find(":", shaderPos); - if (colonPos != Containers::String::npos) { - size_t quoteStart = jsonStr.Find("\"", colonPos); - size_t quoteEnd = jsonStr.Find("\"", quoteStart + 1); - if (quoteStart != Containers::String::npos && quoteEnd != Containers::String::npos) { - Containers::String shaderPath = jsonStr.Substring(quoteStart + 1, quoteEnd - quoteStart - 1); - auto shaderHandle = ResourceManager::Get().Load(shaderPath); - if (shaderHandle.IsValid()) { - material->SetShader(shaderHandle); - } - } - } - } - + material->m_isValid = true; - material->m_memorySize = sizeof(Material) + material->m_name.Length() + material->m_path.Length(); - + material->RecalculateMemorySize(); return LoadResult(material); } @@ -66,6 +296,59 @@ ImportSettings* MaterialLoader::GetDefaultSettings() const { } bool MaterialLoader::ParseMaterialData(const Containers::Array& data, Material* material) { + if (material == nullptr) { + return false; + } + + const std::string jsonText = ToStdString(data); + + Containers::String shaderPath; + if (HasKey(jsonText, "shader")) { + if (!TryParseStringValue(jsonText, "shader", shaderPath)) { + return false; + } + + const ResourceHandle shaderHandle = LoadShaderHandle(shaderPath); + if (shaderHandle.IsValid()) { + material->SetShader(shaderHandle); + } + } + + Containers::String shaderPass; + if (HasKey(jsonText, "shaderPass")) { + if (!TryParseStringValue(jsonText, "shaderPass", shaderPass)) { + return false; + } + material->SetShaderPass(shaderPass); + } else if (HasKey(jsonText, "pass")) { + if (!TryParseStringValue(jsonText, "pass", shaderPass)) { + return false; + } + material->SetShaderPass(shaderPass); + } + + if (HasKey(jsonText, "renderQueue")) { + Core::int32 renderQueue = 0; + if (TryParseIntValue(jsonText, "renderQueue", renderQueue)) { + material->SetRenderQueue(renderQueue); + } else { + Containers::String renderQueueName; + if (!TryParseStringValue(jsonText, "renderQueue", renderQueueName) || + !TryParseRenderQueueName(renderQueueName, renderQueue)) { + return false; + } + + material->SetRenderQueue(renderQueue); + } + } + + if (HasKey(jsonText, "tags")) { + std::string tagObject; + if (!TryExtractObject(jsonText, "tags", tagObject) || !TryParseTagMap(tagObject, material)) { + return false; + } + } + return true; } diff --git a/tests/Resources/Material/test_material.cpp b/tests/Resources/Material/test_material.cpp index 75a25c06..ec56aec2 100644 --- a/tests/Resources/Material/test_material.cpp +++ b/tests/Resources/Material/test_material.cpp @@ -34,6 +34,76 @@ TEST(Material, SetGetShader) { EXPECT_EQ(material.GetShader(), shader); } +TEST(Material, DefaultRenderMetadata) { + Material material; + EXPECT_EQ(material.GetRenderQueue(), static_cast(MaterialRenderQueue::Geometry)); + EXPECT_TRUE(material.GetShaderPass().Empty()); + EXPECT_EQ(material.GetTagCount(), 0u); +} + +TEST(Material, SetGetRenderQueue) { + Material material; + + material.SetRenderQueue(MaterialRenderQueue::Transparent); + EXPECT_EQ(material.GetRenderQueue(), static_cast(MaterialRenderQueue::Transparent)); + + material.SetRenderQueue(2600); + EXPECT_EQ(material.GetRenderQueue(), 2600); +} + +TEST(Material, SetGetShaderPass) { + Material material; + + material.SetShaderPass("ForwardLit"); + EXPECT_EQ(material.GetShaderPass(), "ForwardLit"); +} + +TEST(Material, SetGetTags) { + Material material; + + material.SetTag("LightMode", "ForwardBase"); + material.SetTag("RenderType", "Opaque"); + + EXPECT_TRUE(material.HasTag("LightMode")); + EXPECT_EQ(material.GetTag("LightMode"), "ForwardBase"); + EXPECT_EQ(material.GetTag("RenderType"), "Opaque"); + EXPECT_EQ(material.GetTagCount(), 2u); +} + +TEST(Material, SetTagReplacesExistingValue) { + Material material; + + material.SetTag("LightMode", "ForwardBase"); + material.SetTag("LightMode", "ShadowCaster"); + + EXPECT_EQ(material.GetTagCount(), 1u); + EXPECT_EQ(material.GetTag("LightMode"), "ShadowCaster"); +} + +TEST(Material, RemoveTag) { + Material material; + + material.SetTag("LightMode", "ForwardBase"); + EXPECT_TRUE(material.HasTag("LightMode")); + + material.RemoveTag("LightMode"); + EXPECT_FALSE(material.HasTag("LightMode")); + EXPECT_TRUE(material.GetTag("LightMode").Empty()); +} + +TEST(Material, ClearTags) { + Material material; + + material.SetTag("LightMode", "ForwardBase"); + material.SetTag("RenderType", "Opaque"); + ASSERT_EQ(material.GetTagCount(), 2u); + + material.ClearTags(); + EXPECT_EQ(material.GetTagCount(), 0u); + EXPECT_FALSE(material.HasTag("LightMode")); + EXPECT_FALSE(material.HasTag("RenderType")); +} + TEST(Material, SetGetFloat) { Material material; diff --git a/tests/Resources/Material/test_material_loader.cpp b/tests/Resources/Material/test_material_loader.cpp index 47dff409..8ecb3a18 100644 --- a/tests/Resources/Material/test_material_loader.cpp +++ b/tests/Resources/Material/test_material_loader.cpp @@ -1,8 +1,13 @@ #include #include +#include #include #include +#include +#include +#include + using namespace XCEngine::Resources; using namespace XCEngine::Containers; @@ -33,4 +38,76 @@ TEST(MaterialLoader, LoadInvalidPath) { EXPECT_FALSE(result); } +TEST(MaterialLoader, ResourceManagerRegistersMaterialAndShaderLoaders) { + ResourceManager& manager = ResourceManager::Get(); + manager.Initialize(); + + EXPECT_NE(manager.GetLoader(ResourceType::Material), nullptr); + EXPECT_NE(manager.GetLoader(ResourceType::Shader), nullptr); + + manager.Shutdown(); +} + +TEST(MaterialLoader, LoadValidMaterialParsesRenderMetadata) { + const std::filesystem::path shaderPath = + std::filesystem::current_path() / "material_loader_valid_shader.hlsl"; + const std::filesystem::path materialPath = + std::filesystem::current_path() / "material_loader_valid.material"; + + { + std::ofstream shaderFile(shaderPath); + ASSERT_TRUE(shaderFile.is_open()); + shaderFile << "float4 MainPS() : SV_TARGET { return float4(1, 1, 1, 1); }"; + } + + { + std::ofstream materialFile(materialPath); + ASSERT_TRUE(materialFile.is_open()); + materialFile << "{\n"; + materialFile << " \"shader\": \"" << shaderPath.generic_string() << "\",\n"; + materialFile << " \"renderQueue\": \"Transparent\",\n"; + materialFile << " \"shaderPass\": \"ForwardLit\",\n"; + materialFile << " \"tags\": {\n"; + materialFile << " \"LightMode\": \"ForwardBase\",\n"; + materialFile << " \"RenderType\": \"Transparent\"\n"; + materialFile << " }\n"; + materialFile << "}"; + } + + MaterialLoader loader; + LoadResult result = loader.Load(materialPath.string().c_str()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + Material* material = static_cast(result.resource); + ASSERT_NE(material, nullptr); + EXPECT_TRUE(material->IsValid()); + EXPECT_NE(material->GetShader(), nullptr); + EXPECT_EQ(material->GetRenderQueue(), static_cast(MaterialRenderQueue::Transparent)); + EXPECT_EQ(material->GetShaderPass(), "ForwardLit"); + EXPECT_EQ(material->GetTag("LightMode"), "ForwardBase"); + EXPECT_EQ(material->GetTag("RenderType"), "Transparent"); + + delete material; + std::remove(materialPath.string().c_str()); + std::remove(shaderPath.string().c_str()); +} + +TEST(MaterialLoader, RejectsUnknownRenderQueueName) { + const std::filesystem::path materialPath = + std::filesystem::current_path() / "material_loader_invalid_queue.material"; + + { + std::ofstream materialFile(materialPath); + ASSERT_TRUE(materialFile.is_open()); + materialFile << "{ \"renderQueue\": \"NotAQueue\" }"; + } + + MaterialLoader loader; + LoadResult result = loader.Load(materialPath.string().c_str()); + EXPECT_FALSE(result); + + std::remove(materialPath.string().c_str()); +} + } // namespace