Add material render metadata and loader parsing

This commit is contained in:
2026-03-27 00:30:49 +08:00
parent c97510ed5b
commit f68da2e3f9
6 changed files with 586 additions and 26 deletions

View File

@@ -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<class Shader>& 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<Core::uint32>(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<Core::uint8>& 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<class Shader> m_shader;
Core::int32 m_renderQueue = static_cast<Core::int32>(MaterialRenderQueue::Geometry);
Containers::String m_shaderPass;
struct TagEntry {
Containers::String name;
Containers::String value;
};
Containers::Array<TagEntry> m_tags;
Containers::HashMap<Containers::String, MaterialProperty> m_properties;
Containers::Array<Core::uint8> m_constantBufferData;

View File

@@ -1,10 +1,26 @@
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Asset/ResourceHandle.h>
#include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Resources/Material/MaterialLoader.h>
#include <XCEngine/Resources/Shader/ShaderLoader.h>
namespace XCEngine {
namespace Resources {
namespace {
template<typename TLoader>
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<AsyncLoader>();
m_asyncLoader->Initialize(2);
RegisterBuiltinLoader(*this, g_materialLoader);
RegisterBuiltinLoader(*this, g_shaderLoader);
}
void ResourceManager::Shutdown() {

View File

@@ -11,6 +11,9 @@ Material::~Material() = default;
void Material::Release() {
m_shader.Reset();
m_renderQueue = static_cast<Core::int32>(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>& shader) {
UpdateMemorySize();
}
void Material::SetRenderQueue(Core::int32 renderQueue) {
m_renderQueue = renderQueue;
}
void Material::SetRenderQueue(MaterialRenderQueue renderQueue) {
SetRenderQueue(static_cast<Core::int32>(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();
}

View File

@@ -1,10 +1,256 @@
#include <XCEngine/Resources/Material/MaterialLoader.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Resources/Shader/ShaderLoader.h>
#include <cctype>
#include <cstdlib>
#include <string>
namespace XCEngine {
namespace Resources {
namespace {
std::string ToStdString(const Containers::Array<Core::uint8>& data) {
return std::string(reinterpret_cast<const char*>(data.Data()), data.Size());
}
size_t SkipWhitespace(const std::string& text, size_t pos) {
while (pos < text.size() && std::isspace(static_cast<unsigned char>(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<unsigned char>(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<Core::int32>(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<Core::int32>(MaterialRenderQueue::Background);
return true;
}
if (normalized == "geometry" || normalized == "opaque") {
outQueue = static_cast<Core::int32>(MaterialRenderQueue::Geometry);
return true;
}
if (normalized == "alphatest" || normalized == "alpha_test") {
outQueue = static_cast<Core::int32>(MaterialRenderQueue::AlphaTest);
return true;
}
if (normalized == "transparent") {
outQueue = static_cast<Core::int32>(MaterialRenderQueue::Transparent);
return true;
}
if (normalized == "overlay") {
outQueue = static_cast<Core::int32>(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<Shader> LoadShaderHandle(const Containers::String& shaderPath) {
ResourceHandle<Shader> shader = ResourceManager::Get().Load<Shader>(shaderPath);
if (shader.IsValid()) {
return shader;
}
ShaderLoader shaderLoader;
LoadResult shaderResult = shaderLoader.Load(shaderPath);
if (!shaderResult || shaderResult.resource == nullptr) {
return ResourceHandle<Shader>();
}
return ResourceHandle<Shader>(static_cast<Shader*>(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<Core::uint8> 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<char>(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<Shader>(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<Core::uint8>& 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<Shader> 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;
}

View File

@@ -34,6 +34,76 @@ TEST(Material, SetGetShader) {
EXPECT_EQ(material.GetShader(), shader);
}
TEST(Material, DefaultRenderMetadata) {
Material material;
EXPECT_EQ(material.GetRenderQueue(), static_cast<XCEngine::Core::int32>(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<XCEngine::Core::int32>(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;

View File

@@ -1,8 +1,13 @@
#include <gtest/gtest.h>
#include <XCEngine/Resources/Material/MaterialLoader.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Core/Containers/Array.h>
#include <cstdio>
#include <filesystem>
#include <fstream>
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<Material*>(result.resource);
ASSERT_NE(material, nullptr);
EXPECT_TRUE(material->IsValid());
EXPECT_NE(material->GetShader(), nullptr);
EXPECT_EQ(material->GetRenderQueue(), static_cast<XCEngine::Core::int32>(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