diff --git a/docs/plan/Shader与Material系统下一阶段计划.md b/docs/plan/Shader与Material系统下一阶段计划.md index 732df75c..de169861 100644 --- a/docs/plan/Shader与Material系统下一阶段计划.md +++ b/docs/plan/Shader与Material系统下一阶段计划.md @@ -308,6 +308,16 @@ Unity-like Shader Authoring (.shader) - material 仍然缺少基于 shader property 的正式类型校验、默认值回退和资源映射 - renderer 目前虽然能消费 pass resources,但 material binding 仍偏 builtin-forward 特判 +当前进展(`2026-04-03`): + +- 已完成:shader schema 驱动的 property 类型校验与默认值回退 +- 已完成:source `.material` 的 `properties` authoring 入口 +- 已完成:material constant layout runtime contract + - `Material` 现在会生成正式的 constant layout 元数据 + - layout 字段包含 `name / type / offset / size / alignedSize` + - renderer 读取的已不再只是裸字节 payload,而是 `layout + payload` 组合 +- 下一步:把 texture / sampler / constant resource mapping 从 builtin forward 特判继续推进到更通用的 pass contract + ### 阶段 C:把 Pass Binding 扩展为正式材质执行链路 目标: diff --git a/engine/include/XCEngine/Rendering/RenderMaterialUtility.h b/engine/include/XCEngine/Rendering/RenderMaterialUtility.h index 2c2b1301..6beba568 100644 --- a/engine/include/XCEngine/Rendering/RenderMaterialUtility.h +++ b/engine/include/XCEngine/Rendering/RenderMaterialUtility.h @@ -7,6 +7,8 @@ #include #include +#include + namespace XCEngine { namespace Rendering { @@ -122,6 +124,26 @@ struct BuiltinForwardMaterialData { Math::Vector4 baseColorFactor = Math::Vector4::One(); }; +struct MaterialConstantLayoutView { + const Resources::MaterialConstantFieldDesc* fields = nullptr; + size_t count = 0; + size_t size = 0; + + bool IsValid() const { + return fields != nullptr && count > 0 && size > 0; + } +}; + +struct MaterialConstantPayloadView { + const void* data = nullptr; + size_t size = 0; + MaterialConstantLayoutView layout = {}; + + bool IsValid() const { + return data != nullptr && size > 0 && layout.IsValid() && layout.size == size; + } +}; + inline const Resources::ShaderPropertyDesc* FindShaderPropertyBySemantic( const Resources::Material* material, const Containers::String& semantic) { @@ -220,6 +242,25 @@ inline BuiltinForwardMaterialData BuildBuiltinForwardMaterialData(const Resource return data; } +inline MaterialConstantPayloadView ResolveSchemaMaterialConstantPayload(const Resources::Material* material) { + if (material == nullptr || material->GetShader() == nullptr) { + return {}; + } + + const Containers::Array& constantLayout = material->GetConstantLayout(); + const Containers::Array& constantBufferData = material->GetConstantBufferData(); + if (constantLayout.Empty() || constantBufferData.Empty()) { + return {}; + } + + MaterialConstantLayoutView layoutView = {}; + layoutView.fields = constantLayout.Data(); + layoutView.count = constantLayout.Size(); + layoutView.size = constantBufferData.Size(); + + return { constantBufferData.Data(), constantBufferData.Size(), layoutView }; +} + inline const Resources::Material* ResolveMaterial( const Components::MeshRendererComponent* meshRenderer, const Resources::Mesh* mesh, diff --git a/engine/include/XCEngine/Resources/Material/Material.h b/engine/include/XCEngine/Resources/Material/Material.h index 222e7a71..d642caae 100644 --- a/engine/include/XCEngine/Resources/Material/Material.h +++ b/engine/include/XCEngine/Resources/Material/Material.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -128,6 +129,14 @@ struct MaterialProperty { MaterialProperty() : type(MaterialPropertyType::Float), refCount(0) {} }; +struct MaterialConstantFieldDesc { + Containers::String name; + MaterialPropertyType type = MaterialPropertyType::Float; + Core::uint32 offset = 0; + Core::uint32 size = 0; + Core::uint32 alignedSize = 0; +}; + struct MaterialTagEntry { Containers::String name; Containers::String value; @@ -143,6 +152,7 @@ struct MaterialTextureBinding { Containers::String name; Core::uint32 slot = 0; ResourceHandle texture; + AssetRef textureRef; Containers::String texturePath; std::shared_ptr pendingLoad; }; @@ -190,6 +200,9 @@ public: void SetInt(const Containers::String& name, Core::int32 value); void SetBool(const Containers::String& name, bool value); void SetTexture(const Containers::String& name, const ResourceHandle& texture); + void SetTextureAssetRef(const Containers::String& name, + const AssetRef& textureRef, + const Containers::String& texturePath = Containers::String()); void SetTexturePath(const Containers::String& name, const Containers::String& texturePath); float GetFloat(const Containers::String& name) const; @@ -201,12 +214,16 @@ public: ResourceHandle GetTexture(const Containers::String& name) const; Core::uint32 GetTextureBindingCount() const { return static_cast(m_textureBindings.Size()); } Containers::String GetTextureBindingName(Core::uint32 index) const; + AssetRef GetTextureBindingAssetRef(Core::uint32 index) const; Containers::String GetTextureBindingPath(Core::uint32 index) const; + ResourceHandle GetTextureBindingLoadedTexture(Core::uint32 index) const; ResourceHandle GetTextureBindingTexture(Core::uint32 index) const; const Containers::Array& GetTextureBindings() const { return m_textureBindings; } std::vector GetProperties() const; const Containers::Array& GetConstantBufferData() const { return m_constantBufferData; } + const Containers::Array& GetConstantLayout() const { return m_constantLayout; } + const MaterialConstantFieldDesc* FindConstantField(const Containers::String& name) const; void UpdateConstantBuffer(); Core::uint64 GetChangeVersion() const { return m_changeVersion; } void RecalculateMemorySize(); @@ -216,6 +233,10 @@ public: void ClearAllProperties(); private: + const ShaderPropertyDesc* FindShaderPropertyDesc(const Containers::String& name) const; + bool CanAssignPropertyType(const Containers::String& name, MaterialPropertyType type) const; + bool ResetPropertyToShaderDefault(const Containers::String& name); + void SyncShaderSchemaProperties(bool removeUnknownProperties); void BeginAsyncTextureLoad(Core::uint32 index); void ResolvePendingTextureBinding(Core::uint32 index); void ResolvePendingTextureBindings(); @@ -228,6 +249,7 @@ private: Containers::String m_shaderPass; Containers::Array m_tags; Containers::HashMap m_properties; + Containers::Array m_constantLayout; Containers::Array m_constantBufferData; Containers::Array m_textureBindings; Core::uint64 m_changeVersion = 1; diff --git a/engine/src/Core/Asset/AssetDatabase.cpp b/engine/src/Core/Asset/AssetDatabase.cpp index a5dff85b..347cf210 100644 --- a/engine/src/Core/Asset/AssetDatabase.cpp +++ b/engine/src/Core/Asset/AssetDatabase.cpp @@ -42,11 +42,42 @@ Containers::String ToContainersString(const std::string& value) { return Containers::String(value.c_str()); } +Containers::String NormalizeArtifactPathString(const fs::path& path); +Containers::String NormalizeArtifactPathString(const Containers::String& path); + +void PopulateResolvedAssetResult(const Containers::String& projectRoot, + const AssetDatabase::SourceAssetRecord& sourceRecord, + const AssetDatabase::ArtifactRecord& artifactRecord, + bool imported, + AssetDatabase::ResolvedAsset& outAsset) { + outAsset = AssetDatabase::ResolvedAsset(); + outAsset.exists = true; + outAsset.artifactReady = true; + outAsset.imported = imported; + outAsset.absolutePath = + ToContainersString((fs::path(projectRoot.CStr()) / sourceRecord.relativePath.CStr()).lexically_normal().generic_string()); + outAsset.relativePath = sourceRecord.relativePath; + outAsset.assetGuid = sourceRecord.guid; + outAsset.resourceType = artifactRecord.resourceType; + outAsset.artifactDirectory = + ToContainersString((fs::path(projectRoot.CStr()) / artifactRecord.artifactDirectory.CStr()).lexically_normal().generic_string()); + outAsset.artifactMainPath = + ToContainersString((fs::path(projectRoot.CStr()) / artifactRecord.mainArtifactPath.CStr()).lexically_normal().generic_string()); + outAsset.mainLocalID = artifactRecord.mainLocalID; +} + +Containers::String NormalizeArtifactPathString(const fs::path& path) { + if (path.empty()) { + return Containers::String(); + } + return ToContainersString(path.lexically_normal().generic_string()); +} + Containers::String NormalizeArtifactPathString(const Containers::String& path) { if (path.Empty()) { return Containers::String(); } - return ToContainersString(fs::path(path.CStr()).lexically_normal().generic_string()); + return NormalizeArtifactPathString(fs::path(path.CStr())); } bool IsProjectRelativePath(const fs::path& path) { @@ -321,11 +352,62 @@ std::vector GatherMaterialProperties(const Material& material) return material.GetProperties(); } +std::string EncodeAssetRef(const AssetRef& assetRef) { + if (!assetRef.IsValid()) { + return std::string(); + } + + return ToStdString(assetRef.assetGuid.ToString()) + "," + + std::to_string(assetRef.localID) + "," + + std::to_string(static_cast(assetRef.resourceType)); +} + +AssetRef ResolveTextureBindingAssetRef( + const Material& material, + Core::uint32 bindingIndex, + const std::unordered_map& textureAssetRefs, + const AssetDatabase* assetDatabase) { + const AssetRef bindingAssetRef = material.GetTextureBindingAssetRef(bindingIndex); + if (bindingAssetRef.IsValid()) { + return bindingAssetRef; + } + + const ResourceHandle textureHandle = material.GetTextureBindingLoadedTexture(bindingIndex); + const Texture* texture = textureHandle.Get(); + if (texture != nullptr) { + const auto textureRefIt = textureAssetRefs.find(texture); + if (textureRefIt != textureAssetRefs.end()) { + return textureRefIt->second; + } + + if (assetDatabase != nullptr && + !texture->GetPath().Empty() && + !HasVirtualPathScheme(texture->GetPath())) { + AssetRef resolvedAssetRef; + if (assetDatabase->TryGetAssetRef(texture->GetPath(), ResourceType::Texture, resolvedAssetRef)) { + return resolvedAssetRef; + } + } + } + + const Containers::String bindingPath = material.GetTextureBindingPath(bindingIndex); + if (assetDatabase != nullptr && + !bindingPath.Empty() && + !HasVirtualPathScheme(bindingPath)) { + AssetRef resolvedAssetRef; + if (assetDatabase->TryGetAssetRef(bindingPath, ResourceType::Texture, resolvedAssetRef)) { + return resolvedAssetRef; + } + } + + return AssetRef(); +} + Containers::String ResolveTextureBindingPath( const Material& material, Core::uint32 bindingIndex, const std::unordered_map& textureArtifactPaths) { - const ResourceHandle textureHandle = material.GetTextureBindingTexture(bindingIndex); + const ResourceHandle textureHandle = material.GetTextureBindingLoadedTexture(bindingIndex); const Texture* texture = textureHandle.Get(); if (texture != nullptr) { const auto textureIt = textureArtifactPaths.find(texture); @@ -344,7 +426,9 @@ Containers::String ResolveTextureBindingPath( bool WriteMaterialArtifactFile( const fs::path& artifactPath, const Material& material, - const std::unordered_map& textureArtifactPaths = {}) { + const std::unordered_map& textureArtifactPaths = {}, + const std::unordered_map& textureAssetRefs = {}, + const AssetDatabase* assetDatabase = nullptr) { std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc); if (!output.is_open()) { return false; @@ -398,8 +482,11 @@ bool WriteMaterialArtifactFile( for (Core::uint32 bindingIndex = 0; bindingIndex < material.GetTextureBindingCount(); ++bindingIndex) { const Containers::String bindingName = material.GetTextureBindingName(bindingIndex); + const AssetRef textureAssetRef = + ResolveTextureBindingAssetRef(material, bindingIndex, textureAssetRefs, assetDatabase); WriteString(output, bindingName); + WriteString(output, Containers::String(EncodeAssetRef(textureAssetRef).c_str())); WriteString(output, ResolveTextureBindingPath(material, bindingIndex, textureArtifactPaths)); } @@ -564,7 +651,6 @@ void AssetDatabase::Initialize(const Containers::String& projectRoot) { LoadSourceAssetDB(); LoadArtifactDB(); ScanAssets(); - SaveArtifactDB(); } void AssetDatabase::Shutdown() { @@ -581,8 +667,8 @@ void AssetDatabase::Shutdown() { m_artifactsByGuid.clear(); } -void AssetDatabase::Refresh() { - ScanAssets(); +AssetDatabase::MaintenanceStats AssetDatabase::Refresh() { + return ScanAssets(); } bool AssetDatabase::ResolvePath(const Containers::String& requestPath, @@ -644,6 +730,25 @@ bool AssetDatabase::TryGetAssetGuid(const Containers::String& requestPath, Asset return outGuid.IsValid(); } +bool AssetDatabase::TryGetImportableResourceType(const Containers::String& requestPath, ResourceType& outType) const { + outType = ResourceType::Unknown; + + Containers::String absolutePath; + Containers::String relativePath; + if (!ResolvePath(requestPath, absolutePath, relativePath) || relativePath.Empty()) { + return false; + } + + std::error_code ec; + const fs::path absoluteFsPath(absolutePath.CStr()); + if (!fs::exists(absoluteFsPath, ec) || fs::is_directory(absoluteFsPath, ec)) { + return false; + } + + outType = GetPrimaryResourceTypeForImporter(GetImporterNameForPath(relativePath, false)); + return outType != ResourceType::Unknown; +} + bool AssetDatabase::TryGetAssetRef(const Containers::String& requestPath, ResourceType resourceType, AssetRef& outRef) const { @@ -658,6 +763,115 @@ bool AssetDatabase::TryGetAssetRef(const Containers::String& requestPath, return true; } +bool AssetDatabase::ReimportAsset(const Containers::String& requestPath, + ResolvedAsset& outAsset, + MaintenanceStats* outStats) { + outAsset = ResolvedAsset(); + if (outStats != nullptr) { + *outStats = MaintenanceStats(); + } + + Containers::String absolutePath; + Containers::String relativePath; + if (!ResolvePath(requestPath, absolutePath, relativePath) || relativePath.Empty()) { + return false; + } + + const fs::path absoluteFsPath(absolutePath.CStr()); + if (!fs::exists(absoluteFsPath) || fs::is_directory(absoluteFsPath)) { + return false; + } + + SourceAssetRecord sourceRecord; + if (!EnsureMetaForPath(absoluteFsPath, false, sourceRecord)) { + return false; + } + + const ResourceType primaryType = GetPrimaryResourceTypeForImporter(sourceRecord.importerName); + if (primaryType == ResourceType::Unknown) { + return false; + } + + ArtifactRecord rebuiltRecord; + if (!ImportAsset(sourceRecord, rebuiltRecord)) { + return false; + } + + m_artifactsByGuid[sourceRecord.guid] = rebuiltRecord; + m_sourcesByGuid[sourceRecord.guid].lastKnownArtifactKey = rebuiltRecord.artifactKey; + m_sourcesByPathKey[ToStdString(MakeKey(sourceRecord.relativePath))].lastKnownArtifactKey = rebuiltRecord.artifactKey; + SaveArtifactDB(); + SaveSourceAssetDB(); + + MaintenanceStats localStats; + localStats.importedAssetCount = 1; + localStats.removedArtifactCount = CleanupOrphanedArtifacts(); + if (outStats != nullptr) { + *outStats = localStats; + } + + PopulateResolvedAssetResult(m_projectRoot, sourceRecord, rebuiltRecord, true, outAsset); + return true; +} + +bool AssetDatabase::ReimportAllAssets(MaintenanceStats* outStats) { + if (outStats != nullptr) { + *outStats = MaintenanceStats(); + } + + std::vector importableRecords; + importableRecords.reserve(m_sourcesByGuid.size()); + + for (const auto& [guid, record] : m_sourcesByGuid) { + (void)guid; + if (record.isFolder) { + continue; + } + + const ResourceType primaryType = GetPrimaryResourceTypeForImporter(record.importerName); + if (primaryType == ResourceType::Unknown) { + continue; + } + + const fs::path sourcePath = fs::path(m_projectRoot.CStr()) / record.relativePath.CStr(); + if (!fs::exists(sourcePath) || fs::is_directory(sourcePath)) { + continue; + } + + importableRecords.push_back(record); + } + + std::sort(importableRecords.begin(), importableRecords.end(), [](const SourceAssetRecord& lhs, const SourceAssetRecord& rhs) { + return ToStdString(lhs.relativePath) < ToStdString(rhs.relativePath); + }); + + bool allSucceeded = true; + MaintenanceStats localStats; + for (const SourceAssetRecord& record : importableRecords) { + ArtifactRecord rebuiltRecord; + if (!ImportAsset(record, rebuiltRecord)) { + Debug::Logger::Get().Error( + Debug::LogCategory::FileSystem, + Containers::String("[AssetDatabase] ReimportAllAssets failed path=") + record.relativePath); + allSucceeded = false; + continue; + } + + m_artifactsByGuid[record.guid] = rebuiltRecord; + m_sourcesByGuid[record.guid].lastKnownArtifactKey = rebuiltRecord.artifactKey; + m_sourcesByPathKey[ToStdString(MakeKey(record.relativePath))].lastKnownArtifactKey = rebuiltRecord.artifactKey; + ++localStats.importedAssetCount; + } + + SaveArtifactDB(); + SaveSourceAssetDB(); + localStats.removedArtifactCount = CleanupOrphanedArtifacts(); + if (outStats != nullptr) { + *outStats = localStats; + } + return allSucceeded; +} + bool AssetDatabase::TryGetPrimaryAssetPath(const AssetGUID& guid, Containers::String& outRelativePath) const { const auto sourceIt = m_sourcesByGuid.find(guid); if (sourceIt == m_sourcesByGuid.end()) { @@ -848,14 +1062,18 @@ void AssetDatabase::SaveArtifactDB() const { } } -void AssetDatabase::ScanAssets() { +AssetDatabase::MaintenanceStats AssetDatabase::ScanAssets() { + MaintenanceStats stats; std::unordered_map seenPaths; const fs::path assetsRootPath(m_assetsRoot.CStr()); if (fs::exists(assetsRootPath)) { ScanAssetPath(assetsRootPath, seenPaths); } RemoveMissingRecords(seenPaths); + stats.removedArtifactCount = CleanupOrphanedArtifacts(); SaveSourceAssetDB(); + SaveArtifactDB(); + return stats; } void AssetDatabase::ScanAssetPath(const fs::path& path, @@ -902,9 +1120,75 @@ void AssetDatabase::RemoveMissingRecords(const std::unordered_map retainedArtifactPathKeys; + retainedArtifactPathKeys.reserve(m_artifactsByGuid.size()); + for (const auto& [guid, record] : m_artifactsByGuid) { + (void)guid; + if (!record.artifactDirectory.Empty()) { + retainedArtifactPathKeys.insert(ToStdString(MakeKey(record.artifactDirectory))); + } + } + + Core::uint32 removedArtifactCount = 0; + for (const auto& shardEntry : fs::directory_iterator(artifactsRoot, ec)) { + if (ec) { + ec.clear(); + break; + } + + const fs::path shardPath = shardEntry.path(); + if (!shardEntry.is_directory()) { + fs::remove_all(shardPath, ec); + if (!ec) { + ++removedArtifactCount; + } + ec.clear(); + continue; + } + + for (const auto& artifactEntry : fs::directory_iterator(shardPath, ec)) { + if (ec) { + ec.clear(); + break; + } + + const Containers::String relativeArtifactPath = NormalizeRelativePath(artifactEntry.path()); + const std::string artifactPathKey = ToStdString(MakeKey(relativeArtifactPath)); + if (!relativeArtifactPath.Empty() && + retainedArtifactPathKeys.find(artifactPathKey) != retainedArtifactPathKeys.end()) { + continue; + } + + fs::remove_all(artifactEntry.path(), ec); + if (!ec) { + ++removedArtifactCount; + } + ec.clear(); + } + + std::error_code shardEc; + if (fs::is_empty(shardPath, shardEc)) { + fs::remove(shardPath, shardEc); + } + } + + if (removedArtifactCount > 0) { + Debug::Logger::Get().Info( + Debug::LogCategory::FileSystem, + Containers::String("[AssetDatabase] Removed orphan artifact entries count=") + + Containers::String(std::to_string(removedArtifactCount).c_str())); + } + + return removedArtifactCount; } bool AssetDatabase::EnsureMetaForPath(const fs::path& sourcePath, @@ -1228,7 +1512,9 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath, m_sourcesByPathKey[ToStdString(MakeKey(sourceRecord.relativePath))].lastKnownArtifactKey = rebuiltRecord.artifactKey; SaveArtifactDB(); SaveSourceAssetDB(); + CleanupOrphanedArtifacts(); artifactRecord = &m_artifactsByGuid[sourceRecord.guid]; + outAsset.imported = true; } if (artifactRecord == nullptr) { @@ -1236,14 +1522,7 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath, } outAsset.exists = true; - outAsset.artifactReady = true; - outAsset.absolutePath = absolutePath; - outAsset.relativePath = sourceRecord.relativePath; - outAsset.assetGuid = sourceRecord.guid; - outAsset.resourceType = artifactRecord->resourceType; - outAsset.artifactDirectory = NormalizePathString(fs::path(m_projectRoot.CStr()) / artifactRecord->artifactDirectory.CStr()); - outAsset.artifactMainPath = NormalizePathString(fs::path(m_projectRoot.CStr()) / artifactRecord->mainArtifactPath.CStr()); - outAsset.mainLocalID = artifactRecord->mainLocalID; + PopulateResolvedAssetResult(m_projectRoot, sourceRecord, *artifactRecord, outAsset.imported, outAsset); if (ShouldTraceAssetPath(requestPath)) { Debug::Logger::Get().Info( @@ -1331,7 +1610,7 @@ bool AssetDatabase::ImportMaterialAsset(const SourceAssetRecord& sourceRecord, } const bool writeOk = - WriteMaterialArtifactFile(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *material); + WriteMaterialArtifactFile(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *material, {}, {}, this); delete material; if (!writeOk) { return false; @@ -1380,6 +1659,7 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord, bool writeOk = true; std::unordered_map textureArtifactPaths; + std::unordered_map textureAssetRefs; for (size_t textureIndex = 0; writeOk && textureIndex < mesh->GetTextures().Size(); ++textureIndex) { Texture* texture = mesh->GetTextures()[textureIndex]; if (texture == nullptr) { @@ -1396,6 +1676,13 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord, } textureArtifactPaths.emplace(texture, textureArtifactPath); + + if (!texture->GetPath().Empty()) { + AssetRef textureAssetRef; + if (TryGetAssetRef(texture->GetPath(), ResourceType::Texture, textureAssetRef)) { + textureAssetRefs.emplace(texture, textureAssetRef); + } + } } std::vector materialArtifactPaths; @@ -1412,7 +1699,9 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord, writeOk = WriteMaterialArtifactFile( fs::path(m_projectRoot.CStr()) / materialArtifactPath.CStr(), *material, - textureArtifactPaths); + textureArtifactPaths, + textureAssetRefs, + this); if (!writeOk) { break; } diff --git a/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp b/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp index ba0befa8..85914eef 100644 --- a/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp +++ b/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp @@ -888,7 +888,7 @@ BuiltinForwardPipeline::CachedDescriptorSet* BuiltinForwardPipeline::GetOrCreate Core::uint32 setIndex, Core::uint64 objectId, const Resources::Material* material, - const PerMaterialConstants& materialConstants, + const MaterialConstantPayloadView& materialConstants, RHI::RHIResourceView* textureView) { DynamicDescriptorSetKey key = {}; key.passLayout = passLayoutKey; @@ -911,10 +911,13 @@ BuiltinForwardPipeline::CachedDescriptorSet* BuiltinForwardPipeline::GetOrCreate if (!passLayout.material.IsValid() || passLayout.material.set != setIndex) { return nullptr; } + if (!materialConstants.IsValid()) { + return nullptr; + } cachedDescriptorSet.descriptorSet.set->WriteConstant( passLayout.material.binding, - &materialConstants, - sizeof(materialConstants)); + materialConstants.data, + materialConstants.size); } if (setLayout.usesTexture) { @@ -1046,10 +1049,26 @@ bool BuiltinForwardPipeline::DrawVisibleItem( return false; } - const BuiltinForwardMaterialData materialData = BuildBuiltinForwardMaterialData(material); - const PerMaterialConstants materialConstants = { - materialData.baseColorFactor - }; + MaterialConstantPayloadView materialConstants = ResolveSchemaMaterialConstantPayload(material); + FallbackPerMaterialConstants fallbackMaterialConstants = {}; + if (!materialConstants.IsValid()) { + const BuiltinForwardMaterialData materialData = BuildBuiltinForwardMaterialData(material); + fallbackMaterialConstants.baseColorFactor = materialData.baseColorFactor; + static const Resources::MaterialConstantFieldDesc kFallbackMaterialConstantField = { + Containers::String("baseColorFactor"), + Resources::MaterialPropertyType::Float4, + 0, + sizeof(FallbackPerMaterialConstants), + sizeof(FallbackPerMaterialConstants) + }; + materialConstants.data = &fallbackMaterialConstants; + materialConstants.size = sizeof(fallbackMaterialConstants); + materialConstants.layout = { + &kFallbackMaterialConstantField, + 1, + sizeof(fallbackMaterialConstants) + }; + } if (passLayout->descriptorSetCount > 0) { std::vector descriptorSets(passLayout->descriptorSetCount, nullptr); diff --git a/engine/src/Resources/Material/Material.cpp b/engine/src/Resources/Material/Material.cpp index 476db7f1..a8a114c3 100644 --- a/engine/src/Resources/Material/Material.cpp +++ b/engine/src/Resources/Material/Material.cpp @@ -3,8 +3,11 @@ #include #include +#include #include +#include #include +#include #include namespace XCEngine { @@ -14,6 +17,10 @@ namespace { constexpr size_t kMaterialConstantSlotSize = 16; +bool HasVirtualPathScheme(const Containers::String& path) { + return std::string(path.CStr()).find("://") != std::string::npos; +} + bool IsPackedMaterialPropertyType(MaterialPropertyType type) { switch (type) { case MaterialPropertyType::Float: @@ -33,6 +40,28 @@ bool IsPackedMaterialPropertyType(MaterialPropertyType type) { } } +Core::uint32 GetPackedMaterialPropertySize(MaterialPropertyType type) { + switch (type) { + case MaterialPropertyType::Float: + case MaterialPropertyType::Int: + case MaterialPropertyType::Bool: + return sizeof(Core::uint32); + case MaterialPropertyType::Float2: + case MaterialPropertyType::Int2: + return sizeof(Core::uint32) * 2; + case MaterialPropertyType::Float3: + case MaterialPropertyType::Int3: + return sizeof(Core::uint32) * 3; + case MaterialPropertyType::Float4: + case MaterialPropertyType::Int4: + return sizeof(Core::uint32) * 4; + case MaterialPropertyType::Texture: + case MaterialPropertyType::Cubemap: + default: + return 0; + } +} + void RemoveTextureBindingByName( Containers::Array& textureBindings, const Containers::String& name) { @@ -53,14 +82,187 @@ void RemoveTextureBindingByName( } void EnsureTextureProperty(Containers::HashMap& properties, - const Containers::String& name) { + const Containers::String& name, + MaterialPropertyType type = MaterialPropertyType::Texture) { MaterialProperty prop; prop.name = name; - prop.type = MaterialPropertyType::Texture; + prop.type = type; prop.refCount = 1; properties.Insert(name, prop); } +bool IsTextureMaterialPropertyType(MaterialPropertyType type) { + return type == MaterialPropertyType::Texture || type == MaterialPropertyType::Cubemap; +} + +MaterialPropertyType GetMaterialPropertyTypeForShaderProperty(ShaderPropertyType type) { + switch (type) { + case ShaderPropertyType::Float: + case ShaderPropertyType::Range: + return MaterialPropertyType::Float; + case ShaderPropertyType::Int: + return MaterialPropertyType::Int; + case ShaderPropertyType::Vector: + case ShaderPropertyType::Color: + return MaterialPropertyType::Float4; + case ShaderPropertyType::TextureCube: + return MaterialPropertyType::Cubemap; + case ShaderPropertyType::Texture2D: + default: + return MaterialPropertyType::Texture; + } +} + +bool IsMaterialPropertyCompatibleWithShaderProperty( + MaterialPropertyType materialType, + ShaderPropertyType shaderType) { + switch (shaderType) { + case ShaderPropertyType::Float: + case ShaderPropertyType::Range: + return materialType == MaterialPropertyType::Float; + case ShaderPropertyType::Int: + return materialType == MaterialPropertyType::Int; + case ShaderPropertyType::Vector: + case ShaderPropertyType::Color: + return materialType == MaterialPropertyType::Float4; + case ShaderPropertyType::Texture2D: + return materialType == MaterialPropertyType::Texture; + case ShaderPropertyType::TextureCube: + return materialType == MaterialPropertyType::Texture || + materialType == MaterialPropertyType::Cubemap; + default: + return false; + } +} + +std::string TrimCopy(const std::string& text) { + const auto begin = std::find_if_not(text.begin(), text.end(), [](unsigned char ch) { + return std::isspace(ch) != 0; + }); + if (begin == text.end()) { + return std::string(); + } + + const auto end = std::find_if_not(text.rbegin(), text.rend(), [](unsigned char ch) { + return std::isspace(ch) != 0; + }).base(); + return std::string(begin, end); +} + +bool TryParseFloatList(const Containers::String& value, + float* outValues, + size_t maxValues, + size_t& outCount) { + outCount = 0; + std::string text = TrimCopy(std::string(value.CStr())); + if (text.empty()) { + return false; + } + + if ((text.front() == '(' && text.back() == ')') || + (text.front() == '[' && text.back() == ']') || + (text.front() == '{' && text.back() == '}')) { + text = text.substr(1, text.size() - 2); + } + + const char* cursor = text.c_str(); + char* endPtr = nullptr; + while (*cursor != '\0' && outCount < maxValues) { + while (*cursor != '\0' && + (std::isspace(static_cast(*cursor)) != 0 || *cursor == ',')) { + ++cursor; + } + if (*cursor == '\0') { + break; + } + + const float parsed = std::strtof(cursor, &endPtr); + if (endPtr == cursor) { + return false; + } + + outValues[outCount++] = parsed; + cursor = endPtr; + } + + while (*cursor != '\0') { + if (std::isspace(static_cast(*cursor)) == 0 && *cursor != ',') { + return false; + } + ++cursor; + } + + return outCount > 0; +} + +bool TryParseFloatDefault(const Containers::String& value, float& outValue) { + float values[4] = {}; + size_t count = 0; + if (!TryParseFloatList(value, values, 4, count)) { + return false; + } + + outValue = values[0]; + return true; +} + +bool TryParseIntDefault(const Containers::String& value, Core::int32& outValue) { + const std::string text = TrimCopy(std::string(value.CStr())); + if (text.empty()) { + return false; + } + + char* endPtr = nullptr; + const long parsed = std::strtol(text.c_str(), &endPtr, 10); + if (endPtr == text.c_str()) { + return false; + } + + while (*endPtr != '\0') { + if (std::isspace(static_cast(*endPtr)) == 0) { + return false; + } + ++endPtr; + } + + outValue = static_cast(parsed); + return true; +} + +bool TryBuildDefaultMaterialProperty(const ShaderPropertyDesc& shaderProperty, + MaterialProperty& outProperty) { + outProperty = MaterialProperty(); + outProperty.name = shaderProperty.name; + outProperty.type = GetMaterialPropertyTypeForShaderProperty(shaderProperty.type); + outProperty.refCount = 1; + + switch (shaderProperty.type) { + case ShaderPropertyType::Float: + case ShaderPropertyType::Range: + TryParseFloatDefault(shaderProperty.defaultValue, outProperty.value.floatValue[0]); + return true; + case ShaderPropertyType::Int: + TryParseIntDefault(shaderProperty.defaultValue, outProperty.value.intValue[0]); + return true; + case ShaderPropertyType::Vector: + case ShaderPropertyType::Color: { + float values[4] = {}; + size_t count = 0; + if (TryParseFloatList(shaderProperty.defaultValue, values, 4, count)) { + for (size_t index = 0; index < count && index < 4; ++index) { + outProperty.value.floatValue[index] = values[index]; + } + } + return true; + } + case ShaderPropertyType::Texture2D: + case ShaderPropertyType::TextureCube: + return true; + default: + return false; + } +} + void WritePackedMaterialProperty(Core::uint8* destination, const MaterialProperty& property) { std::memset(destination, 0, kMaterialConstantSlotSize); @@ -114,6 +316,7 @@ void Material::Release() { m_shaderPass.Clear(); m_tags.Clear(); m_properties.Clear(); + m_constantLayout.Clear(); m_textureBindings.Clear(); m_constantBufferData.Clear(); m_changeVersion = 1; @@ -123,7 +326,8 @@ void Material::Release() { void Material::SetShader(const ResourceHandle& shader) { m_shader = shader; - MarkChanged(false); + SyncShaderSchemaProperties(true); + MarkChanged(true); } void Material::SetRenderQueue(Core::int32 renderQueue) { @@ -208,6 +412,10 @@ Containers::String Material::GetTagValue(Core::uint32 index) const { } void Material::SetFloat(const Containers::String& name, float value) { + if (!CanAssignPropertyType(name, MaterialPropertyType::Float)) { + return; + } + RemoveTextureBindingByName(m_textureBindings, name); MaterialProperty prop; prop.name = name; @@ -219,6 +427,10 @@ void Material::SetFloat(const Containers::String& name, float value) { } void Material::SetFloat2(const Containers::String& name, const Math::Vector2& value) { + if (!CanAssignPropertyType(name, MaterialPropertyType::Float2)) { + return; + } + RemoveTextureBindingByName(m_textureBindings, name); MaterialProperty prop; prop.name = name; @@ -231,6 +443,10 @@ void Material::SetFloat2(const Containers::String& name, const Math::Vector2& va } void Material::SetFloat3(const Containers::String& name, const Math::Vector3& value) { + if (!CanAssignPropertyType(name, MaterialPropertyType::Float3)) { + return; + } + RemoveTextureBindingByName(m_textureBindings, name); MaterialProperty prop; prop.name = name; @@ -244,6 +460,10 @@ void Material::SetFloat3(const Containers::String& name, const Math::Vector3& va } void Material::SetFloat4(const Containers::String& name, const Math::Vector4& value) { + if (!CanAssignPropertyType(name, MaterialPropertyType::Float4)) { + return; + } + RemoveTextureBindingByName(m_textureBindings, name); MaterialProperty prop; prop.name = name; @@ -258,6 +478,10 @@ void Material::SetFloat4(const Containers::String& name, const Math::Vector4& va } void Material::SetInt(const Containers::String& name, Core::int32 value) { + if (!CanAssignPropertyType(name, MaterialPropertyType::Int)) { + return; + } + RemoveTextureBindingByName(m_textureBindings, name); MaterialProperty prop; prop.name = name; @@ -269,6 +493,10 @@ void Material::SetInt(const Containers::String& name, Core::int32 value) { } void Material::SetBool(const Containers::String& name, bool value) { + if (!CanAssignPropertyType(name, MaterialPropertyType::Bool)) { + return; + } + RemoveTextureBindingByName(m_textureBindings, name); MaterialProperty prop; prop.name = name; @@ -280,38 +508,27 @@ void Material::SetBool(const Containers::String& name, bool value) { } void Material::SetTexture(const Containers::String& name, const ResourceHandle& texture) { - EnsureTextureProperty(m_properties, name); + const ShaderPropertyDesc* shaderProperty = FindShaderPropertyDesc(name); + const MaterialPropertyType propertyType = + shaderProperty != nullptr + ? GetMaterialPropertyTypeForShaderProperty(shaderProperty->type) + : MaterialPropertyType::Texture; + if (!CanAssignPropertyType(name, propertyType)) { + return; + } + + EnsureTextureProperty(m_properties, name, propertyType); + + AssetRef textureRef; + Containers::String texturePath = texture.Get() != nullptr ? texture->GetPath() : Containers::String(); + if (!texturePath.Empty() && !HasVirtualPathScheme(texturePath)) { + ResourceManager::Get().TryGetAssetRef(texturePath, ResourceType::Texture, textureRef); + } for (auto& binding : m_textureBindings) { if (binding.name == name) { binding.texture = texture; - binding.texturePath = texture.Get() != nullptr ? texture->GetPath() : Containers::String(); - binding.pendingLoad.reset(); - MarkChanged(false); - return; - } - } - - MaterialTextureBinding binding; - binding.name = name; - binding.slot = static_cast(m_textureBindings.Size()); - binding.texture = texture; - binding.texturePath = texture.Get() != nullptr ? texture->GetPath() : Containers::String(); - m_textureBindings.PushBack(binding); - MarkChanged(false); -} - -void Material::SetTexturePath(const Containers::String& name, const Containers::String& texturePath) { - if (texturePath.Empty()) { - RemoveProperty(name); - return; - } - - EnsureTextureProperty(m_properties, name); - - for (auto& binding : m_textureBindings) { - if (binding.name == name) { - binding.texture.Reset(); + binding.textureRef = textureRef; binding.texturePath = texturePath; binding.pendingLoad.reset(); MarkChanged(false); @@ -322,6 +539,84 @@ void Material::SetTexturePath(const Containers::String& name, const Containers:: MaterialTextureBinding binding; binding.name = name; binding.slot = static_cast(m_textureBindings.Size()); + binding.texture = texture; + binding.textureRef = textureRef; + binding.texturePath = texturePath; + m_textureBindings.PushBack(binding); + MarkChanged(false); +} + +void Material::SetTextureAssetRef(const Containers::String& name, + const AssetRef& textureRef, + const Containers::String& texturePath) { + if (!textureRef.IsValid() && texturePath.Empty()) { + RemoveProperty(name); + return; + } + + const ShaderPropertyDesc* shaderProperty = FindShaderPropertyDesc(name); + const MaterialPropertyType propertyType = + shaderProperty != nullptr + ? GetMaterialPropertyTypeForShaderProperty(shaderProperty->type) + : MaterialPropertyType::Texture; + if (!CanAssignPropertyType(name, propertyType)) { + return; + } + + EnsureTextureProperty(m_properties, name, propertyType); + + for (auto& binding : m_textureBindings) { + if (binding.name == name) { + binding.texture.Reset(); + binding.textureRef = textureRef; + binding.texturePath = texturePath; + binding.pendingLoad.reset(); + MarkChanged(false); + return; + } + } + + MaterialTextureBinding binding; + binding.name = name; + binding.slot = static_cast(m_textureBindings.Size()); + binding.textureRef = textureRef; + binding.texturePath = texturePath; + m_textureBindings.PushBack(binding); + MarkChanged(false); +} + +void Material::SetTexturePath(const Containers::String& name, const Containers::String& texturePath) { + if (texturePath.Empty()) { + RemoveProperty(name); + return; + } + + const ShaderPropertyDesc* shaderProperty = FindShaderPropertyDesc(name); + const MaterialPropertyType propertyType = + shaderProperty != nullptr + ? GetMaterialPropertyTypeForShaderProperty(shaderProperty->type) + : MaterialPropertyType::Texture; + if (!CanAssignPropertyType(name, propertyType)) { + return; + } + + EnsureTextureProperty(m_properties, name, propertyType); + + for (auto& binding : m_textureBindings) { + if (binding.name == name) { + binding.texture.Reset(); + binding.textureRef.Reset(); + binding.texturePath = texturePath; + binding.pendingLoad.reset(); + MarkChanged(false); + return; + } + } + + MaterialTextureBinding binding; + binding.name = name; + binding.slot = static_cast(m_textureBindings.Size()); + binding.textureRef.Reset(); binding.texturePath = texturePath; m_textureBindings.PushBack(binding); MarkChanged(false); @@ -384,7 +679,7 @@ ResourceHandle Material::GetTexture(const Containers::String& name) con if (binding.name == name) { if (binding.texture.Get() == nullptr && binding.pendingLoad == nullptr && - !binding.texturePath.Empty()) { + (!binding.texturePath.Empty() || binding.textureRef.IsValid())) { material->BeginAsyncTextureLoad(bindingIndex); } return binding.texture; @@ -397,10 +692,18 @@ Containers::String Material::GetTextureBindingName(Core::uint32 index) const { return index < m_textureBindings.Size() ? m_textureBindings[index].name : Containers::String(); } +AssetRef Material::GetTextureBindingAssetRef(Core::uint32 index) const { + return index < m_textureBindings.Size() ? m_textureBindings[index].textureRef : AssetRef(); +} + Containers::String Material::GetTextureBindingPath(Core::uint32 index) const { return index < m_textureBindings.Size() ? m_textureBindings[index].texturePath : Containers::String(); } +ResourceHandle Material::GetTextureBindingLoadedTexture(Core::uint32 index) const { + return index < m_textureBindings.Size() ? m_textureBindings[index].texture : ResourceHandle(); +} + ResourceHandle Material::GetTextureBindingTexture(Core::uint32 index) const { Material* material = const_cast(this); material->ResolvePendingTextureBinding(index); @@ -408,7 +711,7 @@ ResourceHandle Material::GetTextureBindingTexture(Core::uint32 index) c MaterialTextureBinding& binding = material->m_textureBindings[index]; if (binding.texture.Get() == nullptr && binding.pendingLoad == nullptr && - !binding.texturePath.Empty()) { + (!binding.texturePath.Empty() || binding.textureRef.IsValid())) { material->BeginAsyncTextureLoad(index); } return binding.texture; @@ -427,25 +730,63 @@ std::vector Material::GetProperties() const { return properties; } -void Material::UpdateConstantBuffer() { - std::vector packedProperties; - const auto pairs = m_properties.GetPairs(); - packedProperties.reserve(pairs.Size()); - for (const auto& pair : pairs) { - if (IsPackedMaterialPropertyType(pair.second.type)) { - packedProperties.push_back(&pair.second); +const MaterialConstantFieldDesc* Material::FindConstantField(const Containers::String& name) const { + for (const MaterialConstantFieldDesc& field : m_constantLayout) { + if (field.name == name) { + return &field; } } - std::sort( - packedProperties.begin(), - packedProperties.end(), - [](const MaterialProperty* left, const MaterialProperty* right) { - return std::strcmp(left->name.CStr(), right->name.CStr()) < 0; - }); + return nullptr; +} + +void Material::UpdateConstantBuffer() { + std::vector packedProperties; + if (m_shader.Get() != nullptr && !m_shader->GetProperties().Empty()) { + packedProperties.reserve(m_shader->GetProperties().Size()); + for (const ShaderPropertyDesc& shaderProperty : m_shader->GetProperties()) { + const MaterialProperty* property = m_properties.Find(shaderProperty.name); + if (property == nullptr || + !IsPackedMaterialPropertyType(property->type) || + !IsMaterialPropertyCompatibleWithShaderProperty(property->type, shaderProperty.type)) { + continue; + } + + packedProperties.push_back(*property); + } + } else { + const auto pairs = m_properties.GetPairs(); + packedProperties.reserve(pairs.Size()); + for (const auto& pair : pairs) { + if (IsPackedMaterialPropertyType(pair.second.type)) { + packedProperties.push_back(pair.second); + } + } + + std::sort( + packedProperties.begin(), + packedProperties.end(), + [](const MaterialProperty& left, const MaterialProperty& right) { + return std::strcmp(left.name.CStr(), right.name.CStr()) < 0; + }); + } + + m_constantLayout.Clear(); + m_constantLayout.Reserve(packedProperties.size()); + Core::uint32 currentOffset = 0; + for (const MaterialProperty& property : packedProperties) { + MaterialConstantFieldDesc field; + field.name = property.name; + field.type = property.type; + field.offset = currentOffset; + field.size = GetPackedMaterialPropertySize(property.type); + field.alignedSize = static_cast(kMaterialConstantSlotSize); + m_constantLayout.PushBack(field); + currentOffset += field.alignedSize; + } m_constantBufferData.Clear(); - m_constantBufferData.Resize(packedProperties.size() * kMaterialConstantSlotSize); + m_constantBufferData.Resize(static_cast(currentOffset)); if (!packedProperties.empty()) { std::memset(m_constantBufferData.Data(), 0, m_constantBufferData.Size()); } @@ -453,7 +794,7 @@ void Material::UpdateConstantBuffer() { for (size_t propertyIndex = 0; propertyIndex < packedProperties.size(); ++propertyIndex) { WritePackedMaterialProperty( m_constantBufferData.Data() + propertyIndex * kMaterialConstantSlotSize, - *packedProperties[propertyIndex]); + packedProperties[propertyIndex]); } UpdateMemorySize(); } @@ -468,7 +809,15 @@ void Material::BeginAsyncTextureLoad(Core::uint32 index) { } MaterialTextureBinding& binding = m_textureBindings[index]; - if (binding.texture.Get() != nullptr || binding.texturePath.Empty() || binding.pendingLoad != nullptr) { + if (binding.texture.Get() != nullptr || binding.pendingLoad != nullptr) { + return; + } + + if (binding.texturePath.Empty() && binding.textureRef.IsValid()) { + ResourceManager::Get().TryResolveAssetPath(binding.textureRef, binding.texturePath); + } + + if (binding.texturePath.Empty()) { return; } @@ -505,7 +854,9 @@ void Material::ResolvePendingTextureBinding(Core::uint32 index) { binding.texture = ResourceHandle(static_cast(completedLoad->resource)); if (binding.texture.Get() != nullptr) { - binding.texturePath = binding.texture->GetPath(); + if (binding.texturePath.Empty()) { + binding.texturePath = binding.texture->GetPath(); + } } } @@ -520,6 +871,11 @@ bool Material::HasProperty(const Containers::String& name) const { } void Material::RemoveProperty(const Containers::String& name) { + if (ResetPropertyToShaderDefault(name)) { + MarkChanged(true); + return; + } + const MaterialProperty* property = m_properties.Find(name); const bool removeTextureBinding = property != nullptr && @@ -536,9 +892,79 @@ void Material::RemoveProperty(const Containers::String& name) { void Material::ClearAllProperties() { m_properties.Clear(); + m_constantLayout.Clear(); m_textureBindings.Clear(); m_constantBufferData.Clear(); - MarkChanged(false); + SyncShaderSchemaProperties(false); + MarkChanged(true); +} + +const ShaderPropertyDesc* Material::FindShaderPropertyDesc(const Containers::String& name) const { + if (m_shader.Get() == nullptr) { + return nullptr; + } + + return m_shader->FindProperty(name); +} + +bool Material::CanAssignPropertyType(const Containers::String& name, MaterialPropertyType type) const { + const ShaderPropertyDesc* shaderProperty = FindShaderPropertyDesc(name); + if (shaderProperty == nullptr) { + return m_shader.Get() == nullptr; + } + + return IsMaterialPropertyCompatibleWithShaderProperty(type, shaderProperty->type); +} + +bool Material::ResetPropertyToShaderDefault(const Containers::String& name) { + const ShaderPropertyDesc* shaderProperty = FindShaderPropertyDesc(name); + if (shaderProperty == nullptr) { + return false; + } + + MaterialProperty defaultProperty; + if (!TryBuildDefaultMaterialProperty(*shaderProperty, defaultProperty)) { + return false; + } + + RemoveTextureBindingByName(m_textureBindings, name); + m_properties.Insert(name, defaultProperty); + return true; +} + +void Material::SyncShaderSchemaProperties(bool removeUnknownProperties) { + if (m_shader.Get() == nullptr) { + return; + } + + if (removeUnknownProperties) { + std::vector unknownProperties; + const auto pairs = m_properties.GetPairs(); + unknownProperties.reserve(pairs.Size()); + for (const auto& pair : pairs) { + if (FindShaderPropertyDesc(pair.first) == nullptr) { + unknownProperties.push_back(pair.first); + } + } + + for (const Containers::String& propertyName : unknownProperties) { + m_properties.Erase(propertyName); + RemoveTextureBindingByName(m_textureBindings, propertyName); + } + } + + for (const ShaderPropertyDesc& shaderProperty : m_shader->GetProperties()) { + const MaterialProperty* property = m_properties.Find(shaderProperty.name); + if (property == nullptr || + !IsMaterialPropertyCompatibleWithShaderProperty(property->type, shaderProperty.type)) { + ResetPropertyToShaderDefault(shaderProperty.name); + continue; + } + + if (!IsTextureMaterialPropertyType(property->type)) { + RemoveTextureBindingByName(m_textureBindings, shaderProperty.name); + } + } } void Material::MarkChanged(bool updateConstantBuffer) { @@ -553,6 +979,7 @@ void Material::MarkChanged(bool updateConstantBuffer) { void Material::UpdateMemorySize() { m_memorySize = m_constantBufferData.Size() + + m_constantLayout.Size() * sizeof(MaterialConstantFieldDesc) + sizeof(MaterialRenderState) + m_shaderPass.Length() + m_tags.Size() * sizeof(MaterialTagEntry) + @@ -566,6 +993,10 @@ void Material::UpdateMemorySize() { m_memorySize += tag.value.Length(); } + for (const MaterialConstantFieldDesc& field : m_constantLayout) { + m_memorySize += field.name.Length(); + } + for (const auto& binding : m_textureBindings) { m_memorySize += binding.name.Length(); m_memorySize += binding.texturePath.Length(); diff --git a/engine/src/Resources/Material/MaterialLoader.cpp b/engine/src/Resources/Material/MaterialLoader.cpp index bfaef352..39e6a0f8 100644 --- a/engine/src/Resources/Material/MaterialLoader.cpp +++ b/engine/src/Resources/Material/MaterialLoader.cpp @@ -12,6 +12,7 @@ #include #include #include +#include namespace XCEngine { namespace Resources { @@ -284,6 +285,21 @@ bool TryParseBoolValue(const std::string& json, const char* key, bool& outValue) return false; } +bool TryDecodeAssetRef(const Containers::String& value, AssetRef& outRef) { + const std::string text(value.CStr()); + const size_t firstComma = text.find(','); + const size_t secondComma = firstComma == std::string::npos ? std::string::npos : text.find(',', firstComma + 1); + if (firstComma == std::string::npos || secondComma == std::string::npos) { + return false; + } + + outRef.assetGuid = AssetGUID::ParseOrDefault(Containers::String(text.substr(0, firstComma).c_str())); + outRef.localID = + static_cast(std::stoull(text.substr(firstComma + 1, secondComma - firstComma - 1))); + outRef.resourceType = static_cast(std::stoi(text.substr(secondComma + 1))); + return outRef.IsValid(); +} + 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] != '{') { @@ -329,6 +345,461 @@ bool TryExtractObject(const std::string& json, const char* key, std::string& out return false; } +std::string TrimCopy(const std::string& text) { + const size_t first = SkipWhitespace(text, 0); + if (first >= text.size()) { + return std::string(); + } + + size_t last = text.size(); + while (last > first && std::isspace(static_cast(text[last - 1])) != 0) { + --last; + } + + return text.substr(first, last - first); +} + +bool IsJsonValueTerminator(char ch) { + return std::isspace(static_cast(ch)) != 0 || + ch == ',' || + ch == '}' || + ch == ']'; +} + +bool TryExtractDelimitedText(const std::string& text, + size_t valuePos, + char openChar, + char closeChar, + std::string& outValue, + size_t* nextPos = nullptr) { + if (valuePos >= text.size() || text[valuePos] != openChar) { + return false; + } + + bool inString = false; + bool escaped = false; + int depth = 0; + + for (size_t pos = valuePos; pos < text.size(); ++pos) { + const char ch = text[pos]; + if (escaped) { + escaped = false; + continue; + } + + if (ch == '\\') { + escaped = true; + continue; + } + + if (ch == '"') { + inString = !inString; + continue; + } + + if (inString) { + continue; + } + + if (ch == openChar) { + ++depth; + } else if (ch == closeChar) { + --depth; + if (depth == 0) { + outValue = text.substr(valuePos, pos - valuePos + 1); + if (nextPos != nullptr) { + *nextPos = pos + 1; + } + return true; + } + } + } + + return false; +} + +enum class JsonRawValueType : Core::uint8 { + Invalid = 0, + String, + Number, + Bool, + Array, + Object, + Null +}; + +bool TryApplyTexturePath(Material* material, + const Containers::String& textureName, + const Containers::String& texturePath); + +bool TryExtractRawValue(const std::string& text, + size_t valuePos, + std::string& outValue, + JsonRawValueType& outType, + size_t* nextPos = nullptr) { + outValue.clear(); + outType = JsonRawValueType::Invalid; + + valuePos = SkipWhitespace(text, valuePos); + if (valuePos >= text.size()) { + return false; + } + + const char ch = text[valuePos]; + if (ch == '"') { + Containers::String parsed; + size_t endPos = 0; + if (!ParseQuotedString(text, valuePos, parsed, &endPos)) { + return false; + } + + outValue = parsed.CStr(); + outType = JsonRawValueType::String; + if (nextPos != nullptr) { + *nextPos = endPos; + } + return true; + } + + if (ch == '{') { + if (!TryExtractDelimitedText(text, valuePos, '{', '}', outValue, nextPos)) { + return false; + } + outType = JsonRawValueType::Object; + return true; + } + + if (ch == '[') { + if (!TryExtractDelimitedText(text, valuePos, '[', ']', outValue, nextPos)) { + return false; + } + outType = JsonRawValueType::Array; + return true; + } + + auto tryExtractLiteral = [&](const char* literal, JsonRawValueType valueType) { + const size_t literalLength = std::strlen(literal); + if (text.compare(valuePos, literalLength, literal) != 0) { + return false; + } + + const size_t endPos = valuePos + literalLength; + if (endPos < text.size() && !IsJsonValueTerminator(text[endPos])) { + return false; + } + + outValue = literal; + outType = valueType; + if (nextPos != nullptr) { + *nextPos = endPos; + } + return true; + }; + + if (tryExtractLiteral("true", JsonRawValueType::Bool) || + tryExtractLiteral("false", JsonRawValueType::Bool) || + tryExtractLiteral("null", JsonRawValueType::Null)) { + return true; + } + + if (ch == '-' || std::isdigit(static_cast(ch)) != 0) { + size_t endPos = valuePos + 1; + while (endPos < text.size()) { + const char current = text[endPos]; + if (std::isdigit(static_cast(current)) != 0 || + current == '.' || + current == 'e' || + current == 'E' || + current == '+' || + current == '-') { + ++endPos; + continue; + } + break; + } + + outValue = text.substr(valuePos, endPos - valuePos); + outType = JsonRawValueType::Number; + if (nextPos != nullptr) { + *nextPos = endPos; + } + return true; + } + + return false; +} + +bool TryParseFloatText(const std::string& text, float& outValue) { + const std::string trimmed = TrimCopy(text); + if (trimmed.empty()) { + return false; + } + + char* endPtr = nullptr; + outValue = std::strtof(trimmed.c_str(), &endPtr); + if (endPtr == trimmed.c_str()) { + return false; + } + + while (*endPtr != '\0') { + if (std::isspace(static_cast(*endPtr)) == 0) { + return false; + } + ++endPtr; + } + + return true; +} + +bool TryParseIntText(const std::string& text, Core::int32& outValue) { + const std::string trimmed = TrimCopy(text); + if (trimmed.empty()) { + return false; + } + + char* endPtr = nullptr; + const long parsed = std::strtol(trimmed.c_str(), &endPtr, 10); + if (endPtr == trimmed.c_str()) { + return false; + } + + while (*endPtr != '\0') { + if (std::isspace(static_cast(*endPtr)) == 0) { + return false; + } + ++endPtr; + } + + outValue = static_cast(parsed); + return true; +} + +bool TryParseFloatListText(const std::string& text, + float* outValues, + size_t maxValues, + size_t& outCount) { + outCount = 0; + std::string trimmed = TrimCopy(text); + if (trimmed.empty()) { + return false; + } + + if ((trimmed.front() == '[' && trimmed.back() == ']') || + (trimmed.front() == '(' && trimmed.back() == ')') || + (trimmed.front() == '{' && trimmed.back() == '}')) { + trimmed = trimmed.substr(1, trimmed.size() - 2); + } + + const char* cursor = trimmed.c_str(); + while (*cursor != '\0' && outCount < maxValues) { + while (*cursor != '\0' && + (std::isspace(static_cast(*cursor)) != 0 || *cursor == ',')) { + ++cursor; + } + if (*cursor == '\0') { + break; + } + + char* endPtr = nullptr; + const float parsed = std::strtof(cursor, &endPtr); + if (endPtr == cursor) { + return false; + } + + outValues[outCount++] = parsed; + cursor = endPtr; + } + + while (*cursor != '\0') { + if (std::isspace(static_cast(*cursor)) == 0 && *cursor != ',') { + return false; + } + ++cursor; + } + + return outCount > 0; +} + +bool TryApplySchemaMaterialProperty(Material* material, + const ShaderPropertyDesc& shaderProperty, + const std::string& rawValue, + JsonRawValueType rawType) { + if (material == nullptr || shaderProperty.name.Empty()) { + return false; + } + + switch (shaderProperty.type) { + case ShaderPropertyType::Float: + case ShaderPropertyType::Range: { + float value = 0.0f; + if (rawType != JsonRawValueType::Number || !TryParseFloatText(rawValue, value)) { + return false; + } + + material->SetFloat(shaderProperty.name, value); + return true; + } + case ShaderPropertyType::Int: { + Core::int32 value = 0; + if (rawType != JsonRawValueType::Number || !TryParseIntText(rawValue, value)) { + return false; + } + + material->SetInt(shaderProperty.name, value); + return true; + } + case ShaderPropertyType::Vector: + case ShaderPropertyType::Color: { + float values[4] = {}; + size_t count = 0; + if (rawType != JsonRawValueType::Array || + !TryParseFloatListText(rawValue, values, 4, count) || + count > 4) { + return false; + } + + material->SetFloat4( + shaderProperty.name, + Math::Vector4( + values[0], + count > 1 ? values[1] : 0.0f, + count > 2 ? values[2] : 0.0f, + count > 3 ? values[3] : 0.0f)); + return true; + } + case ShaderPropertyType::Texture2D: + case ShaderPropertyType::TextureCube: + return rawType == JsonRawValueType::String && + TryApplyTexturePath(material, shaderProperty.name, Containers::String(rawValue.c_str())); + default: + return false; + } +} + +bool TryApplyInferredMaterialProperty(Material* material, + const Containers::String& propertyName, + const std::string& rawValue, + JsonRawValueType rawType) { + if (material == nullptr || propertyName.Empty()) { + return false; + } + + switch (rawType) { + case JsonRawValueType::Number: { + Core::int32 intValue = 0; + if (TryParseIntText(rawValue, intValue)) { + material->SetInt(propertyName, intValue); + return true; + } + + float floatValue = 0.0f; + if (!TryParseFloatText(rawValue, floatValue)) { + return false; + } + + material->SetFloat(propertyName, floatValue); + return true; + } + case JsonRawValueType::Bool: + material->SetBool(propertyName, rawValue == "true"); + return true; + case JsonRawValueType::Array: { + float values[4] = {}; + size_t count = 0; + if (!TryParseFloatListText(rawValue, values, 4, count) || count > 4) { + return false; + } + + switch (count) { + case 1: + material->SetFloat(propertyName, values[0]); + return true; + case 2: + material->SetFloat2(propertyName, Math::Vector2(values[0], values[1])); + return true; + case 3: + material->SetFloat3(propertyName, Math::Vector3(values[0], values[1], values[2])); + return true; + case 4: + material->SetFloat4(propertyName, Math::Vector4(values[0], values[1], values[2], values[3])); + return true; + default: + return false; + } + } + default: + return false; + } +} + +bool TryParseMaterialPropertiesObject(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 propertyName; + if (!ParseQuotedString(objectText, pos, propertyName, &pos) || propertyName.Empty()) { + return false; + } + + pos = SkipWhitespace(objectText, pos); + if (pos >= objectText.size() || objectText[pos] != ':') { + return false; + } + + std::string rawValue; + JsonRawValueType rawType = JsonRawValueType::Invalid; + pos = SkipWhitespace(objectText, pos + 1); + if (!TryExtractRawValue(objectText, pos, rawValue, rawType, &pos)) { + return false; + } + + const Shader* shader = material->GetShader(); + const ShaderPropertyDesc* shaderProperty = + shader != nullptr ? shader->FindProperty(propertyName) : nullptr; + if (shader != nullptr && shaderProperty == nullptr) { + return false; + } + + if (shaderProperty != nullptr) { + if (!TryApplySchemaMaterialProperty(material, *shaderProperty, rawValue, rawType)) { + return false; + } + } else if (!TryApplyInferredMaterialProperty(material, propertyName, rawValue, rawType)) { + return false; + } + + pos = SkipWhitespace(objectText, pos); + if (pos >= objectText.size()) { + return false; + } + + if (objectText[pos] == ',') { + ++pos; + continue; + } + + if (objectText[pos] == '}') { + return true; + } + + return false; + } + + return false; +} + bool TryParseRenderQueueName(const Containers::String& queueName, Core::int32& outQueue) { const Containers::String normalized = queueName.ToLower(); if (normalized == "background") { @@ -912,7 +1383,7 @@ LoadResult LoadMaterialArtifact(const Containers::String& path) { } const std::string magic(fileHeader.magic, fileHeader.magic + 7); - if (magic != "XCMAT01") { + if (magic != "XCMAT02") { return LoadResult("Invalid material artifact magic: " + path); } @@ -984,14 +1455,23 @@ LoadResult LoadMaterialArtifact(const Containers::String& path) { for (Core::uint32 bindingIndex = 0; bindingIndex < header.textureBindingCount; ++bindingIndex) { Containers::String bindingName; + Containers::String textureRefText; Containers::String texturePath; if (!ReadMaterialArtifactString(data, offset, bindingName) || + !ReadMaterialArtifactString(data, offset, textureRefText) || !ReadMaterialArtifactString(data, offset, texturePath)) { return LoadResult("Failed to read material artifact texture bindings: " + path); } - if (!texturePath.Empty()) { - material->SetTexturePath(bindingName, ResolveArtifactDependencyPath(texturePath, path)); + AssetRef textureRef; + TryDecodeAssetRef(textureRefText, textureRef); + const Containers::String resolvedTexturePath = + texturePath.Empty() ? Containers::String() : ResolveArtifactDependencyPath(texturePath, path); + + if (textureRef.IsValid()) { + material->SetTextureAssetRef(bindingName, textureRef, resolvedTexturePath); + } else if (!resolvedTexturePath.Empty()) { + material->SetTexturePath(bindingName, resolvedTexturePath); } } @@ -1123,6 +1603,14 @@ bool MaterialLoader::ParseMaterialData(const Containers::Array& dat } } + if (HasKey(jsonText, "properties")) { + std::string propertiesObject; + if (!TryExtractObject(jsonText, "properties", propertiesObject) || + !TryParseMaterialPropertiesObject(propertiesObject, material)) { + return false; + } + } + if (!TryParseMaterialTextureBindings(jsonText, material)) { return false; } diff --git a/tests/Rendering/unit/test_render_scene_extractor.cpp b/tests/Rendering/unit/test_render_scene_extractor.cpp index 5916c029..ab09f03a 100644 --- a/tests/Rendering/unit/test_render_scene_extractor.cpp +++ b/tests/Rendering/unit/test_render_scene_extractor.cpp @@ -479,6 +479,55 @@ TEST(RenderMaterialUtility_Test, ResolvesBuiltinForwardMaterialContractFromShade EXPECT_EQ(ResolveBuiltinBaseColorTexture(&material), texture); } +TEST(RenderMaterialUtility_Test, ResolvesBuiltinForwardMaterialContractFromShaderSemanticDefaults) { + auto* shader = new Shader(); + + ShaderPropertyDesc colorProperty = {}; + colorProperty.name = "TintColor"; + colorProperty.displayName = "Tint"; + colorProperty.type = ShaderPropertyType::Color; + colorProperty.defaultValue = "(0.11,0.22,0.33,0.44)"; + colorProperty.semantic = "BaseColor"; + shader->AddProperty(colorProperty); + + Material material; + material.SetShader(ResourceHandle(shader)); + + EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(0.11f, 0.22f, 0.33f, 0.44f)); + EXPECT_EQ(ResolveBuiltinBaseColorTexture(&material), nullptr); +} + +TEST(RenderMaterialUtility_Test, ExposesSchemaDrivenMaterialConstantPayload) { + auto* shader = new Shader(); + + ShaderPropertyDesc colorProperty = {}; + colorProperty.name = "_BaseColor"; + colorProperty.type = ShaderPropertyType::Color; + colorProperty.defaultValue = "(0.25,0.5,0.75,1.0)"; + colorProperty.semantic = "BaseColor"; + shader->AddProperty(colorProperty); + + Material material; + material.SetShader(ResourceHandle(shader)); + + const MaterialConstantPayloadView payload = ResolveSchemaMaterialConstantPayload(&material); + ASSERT_TRUE(payload.IsValid()); + ASSERT_EQ(payload.size, 16u); + ASSERT_TRUE(payload.layout.IsValid()); + ASSERT_EQ(payload.layout.count, 1u); + EXPECT_EQ(payload.layout.size, 16u); + EXPECT_EQ(payload.layout.fields[0].name, "_BaseColor"); + EXPECT_EQ(payload.layout.fields[0].offset, 0u); + EXPECT_EQ(payload.layout.fields[0].size, 16u); + EXPECT_EQ(payload.layout.fields[0].alignedSize, 16u); + + const float* values = static_cast(payload.data); + EXPECT_FLOAT_EQ(values[0], 0.25f); + EXPECT_FLOAT_EQ(values[1], 0.5f); + EXPECT_FLOAT_EQ(values[2], 0.75f); + EXPECT_FLOAT_EQ(values[3], 1.0f); +} + TEST(RenderMaterialUtility_Test, UsesOpacityOnlyWhenBaseColorFactorIsMissing) { Material material; material.SetFloat("opacity", 0.35f); diff --git a/tests/Resources/Material/test_material.cpp b/tests/Resources/Material/test_material.cpp index c42689e6..58dcb368 100644 --- a/tests/Resources/Material/test_material.cpp +++ b/tests/Resources/Material/test_material.cpp @@ -12,6 +12,35 @@ using namespace XCEngine::Math; namespace { +Shader* CreateMaterialSchemaShader() { + auto* shader = new Shader(); + + ShaderPropertyDesc baseColor = {}; + baseColor.name = "_BaseColor"; + baseColor.displayName = "Base Color"; + baseColor.type = ShaderPropertyType::Color; + baseColor.defaultValue = "(1.0,0.5,0.25,1.0)"; + baseColor.semantic = "BaseColor"; + shader->AddProperty(baseColor); + + ShaderPropertyDesc metallic = {}; + metallic.name = "_Metallic"; + metallic.displayName = "Metallic"; + metallic.type = ShaderPropertyType::Float; + metallic.defaultValue = "0.7"; + shader->AddProperty(metallic); + + ShaderPropertyDesc baseMap = {}; + baseMap.name = "_MainTex"; + baseMap.displayName = "Base Map"; + baseMap.type = ShaderPropertyType::Texture2D; + baseMap.defaultValue = "white"; + baseMap.semantic = "BaseColorTexture"; + shader->AddProperty(baseMap); + + return shader; +} + TEST(Material, DefaultConstructor) { Material material; EXPECT_EQ(material.GetType(), ResourceType::Material); @@ -225,6 +254,28 @@ TEST(Material, SetTextureReplacesExistingBinding) { EXPECT_EQ(material.GetTexture("uDiffuse").Get(), secondTexture); } +TEST(Material, SetTextureAssetRefStoresStableBindingMetadata) { + Material material; + + AssetRef textureRef; + textureRef.assetGuid = AssetGUID(1, 2); + textureRef.localID = kMainAssetLocalID; + textureRef.resourceType = ResourceType::Texture; + + material.SetTextureAssetRef("uDiffuse", textureRef, "Assets/diffuse.bmp"); + + ASSERT_EQ(material.GetTextureBindingCount(), 1u); + EXPECT_TRUE(material.HasProperty("uDiffuse")); + EXPECT_EQ(material.GetTextureBindingName(0), "uDiffuse"); + EXPECT_EQ(material.GetTextureBindingPath(0), "Assets/diffuse.bmp"); + + const AssetRef storedRef = material.GetTextureBindingAssetRef(0); + EXPECT_EQ(storedRef.assetGuid, textureRef.assetGuid); + EXPECT_EQ(storedRef.localID, textureRef.localID); + EXPECT_EQ(storedRef.resourceType, textureRef.resourceType); + EXPECT_FALSE(material.GetTextureBindingLoadedTexture(0).IsValid()); +} + TEST(Material, ChangeVersionIncrementsWhenMaterialMutates) { Material material; const XCEngine::Core::uint64 initialVersion = material.GetChangeVersion(); @@ -243,6 +294,24 @@ TEST(Material, UpdateConstantBufferPacksNumericPropertiesIntoStableSlots) { material.SetFloat4("beta", Vector4(1.0f, 2.0f, 3.0f, 4.0f)); material.SetInt("gamma", 7); + const auto& constantLayout = material.GetConstantLayout(); + ASSERT_EQ(constantLayout.Size(), 3u); + EXPECT_EQ(constantLayout[0].name, "alpha"); + EXPECT_EQ(constantLayout[0].offset, 0u); + EXPECT_EQ(constantLayout[0].size, 4u); + EXPECT_EQ(constantLayout[0].alignedSize, 16u); + EXPECT_EQ(constantLayout[1].name, "beta"); + EXPECT_EQ(constantLayout[1].offset, 16u); + EXPECT_EQ(constantLayout[1].size, 16u); + EXPECT_EQ(constantLayout[2].name, "gamma"); + EXPECT_EQ(constantLayout[2].offset, 32u); + EXPECT_EQ(constantLayout[2].size, 4u); + + const MaterialConstantFieldDesc* betaField = material.FindConstantField("beta"); + ASSERT_NE(betaField, nullptr); + EXPECT_EQ(betaField->offset, 16u); + EXPECT_EQ(betaField->alignedSize, 16u); + const auto& constantBufferData = material.GetConstantBufferData(); ASSERT_EQ(constantBufferData.Size(), 48u); @@ -331,4 +400,138 @@ TEST(Material, ClearAllProperties) { EXPECT_FALSE(material.HasProperty("uIndex")); } +TEST(Material, SetShaderSeedsDefaultsAndRemovePropertyRestoresShaderDefault) { + Material material; + Shader* shader = CreateMaterialSchemaShader(); + + material.SetShader(ResourceHandle(shader)); + + EXPECT_TRUE(material.HasProperty("_BaseColor")); + EXPECT_TRUE(material.HasProperty("_Metallic")); + EXPECT_TRUE(material.HasProperty("_MainTex")); + EXPECT_EQ(material.GetFloat4("_BaseColor"), Vector4(1.0f, 0.5f, 0.25f, 1.0f)); + EXPECT_FLOAT_EQ(material.GetFloat("_Metallic"), 0.7f); + EXPECT_EQ(material.GetTextureBindingCount(), 0u); + + material.SetFloat4("_BaseColor", Vector4(0.2f, 0.3f, 0.4f, 0.5f)); + EXPECT_EQ(material.GetFloat4("_BaseColor"), Vector4(0.2f, 0.3f, 0.4f, 0.5f)); + + material.RemoveProperty("_BaseColor"); + EXPECT_EQ(material.GetFloat4("_BaseColor"), Vector4(1.0f, 0.5f, 0.25f, 1.0f)); +} + +TEST(Material, ClearAllPropertiesWithShaderRestoresSchemaDefaults) { + Material material; + Shader* shader = CreateMaterialSchemaShader(); + + material.SetShader(ResourceHandle(shader)); + material.SetFloat("_Metallic", 0.15f); + material.SetTexture("_MainTex", ResourceHandle(new Texture())); + ASSERT_EQ(material.GetTextureBindingCount(), 1u); + + material.ClearAllProperties(); + + EXPECT_TRUE(material.HasProperty("_BaseColor")); + EXPECT_TRUE(material.HasProperty("_Metallic")); + EXPECT_TRUE(material.HasProperty("_MainTex")); + EXPECT_EQ(material.GetFloat4("_BaseColor"), Vector4(1.0f, 0.5f, 0.25f, 1.0f)); + EXPECT_FLOAT_EQ(material.GetFloat("_Metallic"), 0.7f); + EXPECT_EQ(material.GetTextureBindingCount(), 0u); +} + +TEST(Material, ShaderSchemaRejectsUnknownAndTypeMismatchedAssignments) { + Material material; + Shader* shader = CreateMaterialSchemaShader(); + + material.SetShader(ResourceHandle(shader)); + const Vector4 defaultBaseColor = material.GetFloat4("_BaseColor"); + const float defaultMetallic = material.GetFloat("_Metallic"); + + material.SetFloat("_BaseColor", 0.1f); + material.SetFloat4("_Metallic", Vector4(1.0f, 2.0f, 3.0f, 4.0f)); + material.SetFloat("UnknownProperty", 5.0f); + + EXPECT_EQ(material.GetFloat4("_BaseColor"), defaultBaseColor); + EXPECT_FLOAT_EQ(material.GetFloat("_Metallic"), defaultMetallic); + EXPECT_FALSE(material.HasProperty("UnknownProperty")); +} + +TEST(Material, SwitchingShaderResyncsPropertiesAgainstNewSchema) { + Material material; + + auto* shaderA = new Shader(); + ShaderPropertyDesc sharedA = {}; + sharedA.name = "Shared"; + sharedA.type = ShaderPropertyType::Float; + sharedA.defaultValue = "1.0"; + shaderA->AddProperty(sharedA); + ShaderPropertyDesc onlyA = {}; + onlyA.name = "OnlyA"; + onlyA.type = ShaderPropertyType::Float; + onlyA.defaultValue = "2.0"; + shaderA->AddProperty(onlyA); + + auto* shaderB = new Shader(); + ShaderPropertyDesc sharedB = {}; + sharedB.name = "Shared"; + sharedB.type = ShaderPropertyType::Float; + sharedB.defaultValue = "4.0"; + shaderB->AddProperty(sharedB); + ShaderPropertyDesc onlyB = {}; + onlyB.name = "OnlyB"; + onlyB.type = ShaderPropertyType::Color; + onlyB.defaultValue = "(0.1,0.2,0.3,0.4)"; + shaderB->AddProperty(onlyB); + + material.SetFloat("Legacy", 9.0f); + material.SetShader(ResourceHandle(shaderA)); + material.SetFloat("Shared", 5.0f); + material.SetFloat("OnlyA", 8.0f); + + material.SetShader(ResourceHandle(shaderB)); + + EXPECT_FALSE(material.HasProperty("Legacy")); + EXPECT_FALSE(material.HasProperty("OnlyA")); + EXPECT_TRUE(material.HasProperty("Shared")); + EXPECT_TRUE(material.HasProperty("OnlyB")); + EXPECT_FLOAT_EQ(material.GetFloat("Shared"), 5.0f); + EXPECT_EQ(material.GetFloat4("OnlyB"), Vector4(0.1f, 0.2f, 0.3f, 0.4f)); +} + +TEST(Material, UpdateConstantBufferFollowsShaderSchemaOrderInsteadOfAlphabeticalOrder) { + Material material; + + auto* shader = new Shader(); + ShaderPropertyDesc beta = {}; + beta.name = "beta"; + beta.type = ShaderPropertyType::Float; + beta.defaultValue = "0.0"; + shader->AddProperty(beta); + + ShaderPropertyDesc alpha = {}; + alpha.name = "alpha"; + alpha.type = ShaderPropertyType::Float; + alpha.defaultValue = "0.0"; + shader->AddProperty(alpha); + + material.SetShader(ResourceHandle(shader)); + material.SetFloat("alpha", 10.0f); + material.SetFloat("beta", 20.0f); + + const auto& constantLayout = material.GetConstantLayout(); + ASSERT_EQ(constantLayout.Size(), 2u); + EXPECT_EQ(constantLayout[0].name, "beta"); + EXPECT_EQ(constantLayout[0].offset, 0u); + EXPECT_EQ(constantLayout[1].name, "alpha"); + EXPECT_EQ(constantLayout[1].offset, 16u); + + const auto& constantBufferData = material.GetConstantBufferData(); + ASSERT_EQ(constantBufferData.Size(), 32u); + + const float* firstSlot = reinterpret_cast(constantBufferData.Data()); + const float* secondSlot = reinterpret_cast(constantBufferData.Data() + 16); + EXPECT_FLOAT_EQ(firstSlot[0], 20.0f); + EXPECT_FLOAT_EQ(secondSlot[0], 10.0f); +} + } // namespace diff --git a/tests/Resources/Material/test_material_loader.cpp b/tests/Resources/Material/test_material_loader.cpp index d4d2c539..6c9f3a54 100644 --- a/tests/Resources/Material/test_material_loader.cpp +++ b/tests/Resources/Material/test_material_loader.cpp @@ -66,6 +66,66 @@ void WriteTextFile(const std::filesystem::path& path, const std::string& content ASSERT_TRUE(static_cast(output)); } +std::filesystem::path WriteSchemaMaterialShaderManifest(const std::filesystem::path& rootPath) { + namespace fs = std::filesystem; + + const fs::path shaderDir = rootPath / "Shaders"; + fs::create_directories(shaderDir); + + WriteTextFile(shaderDir / "schema.vert.glsl", "#version 430\nvoid main() {}\n"); + WriteTextFile(shaderDir / "schema.frag.glsl", "#version 430\nvoid main() {}\n"); + + const fs::path manifestPath = shaderDir / "schema.shader"; + std::ofstream manifest(manifestPath, std::ios::binary | std::ios::trunc); + EXPECT_TRUE(manifest.is_open()); + if (!manifest.is_open()) { + return {}; + } + manifest << "{\n"; + manifest << " \"name\": \"SchemaMaterialShader\",\n"; + manifest << " \"properties\": [\n"; + manifest << " {\n"; + manifest << " \"name\": \"_BaseColor\",\n"; + manifest << " \"displayName\": \"Base Color\",\n"; + manifest << " \"type\": \"Color\",\n"; + manifest << " \"defaultValue\": \"(1,0.5,0.25,1)\",\n"; + manifest << " \"semantic\": \"BaseColor\"\n"; + manifest << " },\n"; + manifest << " {\n"; + manifest << " \"name\": \"_Metallic\",\n"; + manifest << " \"displayName\": \"Metallic\",\n"; + manifest << " \"type\": \"Float\",\n"; + manifest << " \"defaultValue\": \"0.7\"\n"; + manifest << " },\n"; + manifest << " {\n"; + manifest << " \"name\": \"_Mode\",\n"; + manifest << " \"displayName\": \"Mode\",\n"; + manifest << " \"type\": \"Int\",\n"; + manifest << " \"defaultValue\": \"2\"\n"; + manifest << " },\n"; + manifest << " {\n"; + manifest << " \"name\": \"_MainTex\",\n"; + manifest << " \"displayName\": \"Main Tex\",\n"; + manifest << " \"type\": \"Texture2D\",\n"; + manifest << " \"defaultValue\": \"white\",\n"; + manifest << " \"semantic\": \"BaseColorTexture\"\n"; + manifest << " }\n"; + manifest << " ],\n"; + manifest << " \"passes\": [\n"; + manifest << " {\n"; + manifest << " \"name\": \"ForwardLit\",\n"; + manifest << " \"variants\": [\n"; + manifest << " { \"stage\": \"Vertex\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"schema.vert.glsl\" },\n"; + manifest << " { \"stage\": \"Fragment\", \"backend\": \"OpenGL\", \"language\": \"GLSL\", \"source\": \"schema.frag.glsl\" }\n"; + manifest << " ]\n"; + manifest << " }\n"; + manifest << " ]\n"; + manifest << "}\n"; + EXPECT_TRUE(static_cast(manifest)); + + return manifestPath; +} + TEST(MaterialLoader, GetResourceType) { MaterialLoader loader; EXPECT_EQ(loader.GetResourceType(), ResourceType::Material); @@ -235,6 +295,135 @@ TEST(MaterialLoader, LoadMaterialWithShaderManifestResolvesShaderPass) { fs::remove_all(shaderRoot); } +TEST(MaterialLoader, LoadMaterialWithPropertiesObjectAppliesTypedOverrides) { + namespace fs = std::filesystem; + + const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_properties_override_test"; + fs::remove_all(rootPath); + + const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath); + ASSERT_FALSE(shaderPath.empty()); + const fs::path materialPath = rootPath / "override.material"; + + WriteTextFile( + materialPath, + "{\n" + " \"shader\": \"" + shaderPath.generic_string() + "\",\n" + " \"shaderPass\": \"ForwardLit\",\n" + " \"properties\": {\n" + " \"_BaseColor\": [0.2, 0.4, 0.6, 0.8],\n" + " \"_Metallic\": 0.15,\n" + " \"_Mode\": 5\n" + " }\n" + "}\n"); + + MaterialLoader loader; + LoadResult result = loader.Load(materialPath.generic_string().c_str()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + auto* material = static_cast(result.resource); + ASSERT_NE(material, nullptr); + ASSERT_NE(material->GetShader(), nullptr); + EXPECT_EQ(material->GetShaderPass(), "ForwardLit"); + EXPECT_EQ(material->GetFloat4("_BaseColor"), XCEngine::Math::Vector4(0.2f, 0.4f, 0.6f, 0.8f)); + EXPECT_FLOAT_EQ(material->GetFloat("_Metallic"), 0.15f); + EXPECT_EQ(material->GetInt("_Mode"), 5); + EXPECT_TRUE(material->HasProperty("_MainTex")); + EXPECT_EQ(material->GetTextureBindingCount(), 0u); + + delete material; + fs::remove_all(rootPath); +} + +TEST(MaterialLoader, RejectsUnknownPropertyAgainstShaderSchema) { + namespace fs = std::filesystem; + + const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_properties_unknown_test"; + fs::remove_all(rootPath); + + const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath); + ASSERT_FALSE(shaderPath.empty()); + const fs::path materialPath = rootPath / "unknown_property.material"; + + WriteTextFile( + materialPath, + "{\n" + " \"shader\": \"" + shaderPath.generic_string() + "\",\n" + " \"properties\": {\n" + " \"_Unknown\": 1.0\n" + " }\n" + "}\n"); + + MaterialLoader loader; + LoadResult result = loader.Load(materialPath.generic_string().c_str()); + EXPECT_FALSE(result); + + fs::remove_all(rootPath); +} + +TEST(MaterialLoader, RejectsTypeMismatchAgainstShaderSchema) { + namespace fs = std::filesystem; + + const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_properties_mismatch_test"; + fs::remove_all(rootPath); + + const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath); + ASSERT_FALSE(shaderPath.empty()); + const fs::path materialPath = rootPath / "type_mismatch.material"; + + WriteTextFile( + materialPath, + "{\n" + " \"shader\": \"" + shaderPath.generic_string() + "\",\n" + " \"properties\": {\n" + " \"_BaseColor\": 1.0\n" + " }\n" + "}\n"); + + MaterialLoader loader; + LoadResult result = loader.Load(materialPath.generic_string().c_str()); + EXPECT_FALSE(result); + + fs::remove_all(rootPath); +} + +TEST(MaterialLoader, LoadMaterialWithPropertiesObjectPreservesShaderDefaultsForOmittedValues) { + namespace fs = std::filesystem; + + const fs::path rootPath = fs::temp_directory_path() / "xc_material_loader_properties_defaults_test"; + fs::remove_all(rootPath); + + const fs::path shaderPath = WriteSchemaMaterialShaderManifest(rootPath); + ASSERT_FALSE(shaderPath.empty()); + const fs::path materialPath = rootPath / "defaults.material"; + + WriteTextFile( + materialPath, + "{\n" + " \"shader\": \"" + shaderPath.generic_string() + "\",\n" + " \"properties\": {\n" + " \"_Metallic\": 0.33\n" + " }\n" + "}\n"); + + MaterialLoader loader; + LoadResult result = loader.Load(materialPath.generic_string().c_str()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + auto* material = static_cast(result.resource); + ASSERT_NE(material, nullptr); + EXPECT_EQ(material->GetFloat4("_BaseColor"), XCEngine::Math::Vector4(1.0f, 0.5f, 0.25f, 1.0f)); + EXPECT_FLOAT_EQ(material->GetFloat("_Metallic"), 0.33f); + EXPECT_EQ(material->GetInt("_Mode"), 2); + EXPECT_TRUE(material->HasProperty("_MainTex")); + EXPECT_EQ(material->GetTextureBindingCount(), 0u); + + delete material; + fs::remove_all(rootPath); +} + TEST(MaterialLoader, RejectsUnknownRenderQueueName) { const std::filesystem::path materialPath = std::filesystem::current_path() / "material_loader_invalid_queue.material"; @@ -380,6 +569,7 @@ TEST(MaterialLoader, ResourceManagerLoadsProjectMaterialTextureAsLazyDependency) ASSERT_TRUE(materialHandle.IsValid()); ASSERT_EQ(materialHandle->GetTextureBindingCount(), 1u); EXPECT_EQ(materialHandle->GetTextureBindingName(0), "baseColorTexture"); + EXPECT_TRUE(materialHandle->GetTextureBindingAssetRef(0).IsValid()); EXPECT_EQ( fs::path(materialHandle->GetTextureBindingPath(0).CStr()).lexically_normal().generic_string(), (projectRoot / "Assets" / "checker.bmp").lexically_normal().generic_string()); @@ -542,6 +732,9 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) { assetsDir / "checker.bmp", fs::copy_options::overwrite_existing); + manager.SetResourceRoot(projectRoot.string().c_str()); + manager.RefreshProjectAssets(); + { std::ofstream output(materialArtifactPath, std::ios::binary | std::ios::trunc); ASSERT_TRUE(output.is_open()); @@ -558,11 +751,18 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) { header.textureBindingCount = 1; output.write(reinterpret_cast(&header), sizeof(header)); - WriteArtifactString(output, "baseColorTexture"); - WriteArtifactString(output, "Assets/checker.bmp"); - } + AssetRef textureRef; + ASSERT_TRUE(manager.TryGetAssetRef("Assets/checker.bmp", ResourceType::Texture, textureRef)); + ASSERT_TRUE(textureRef.IsValid()); + const String encodedTextureRef = + textureRef.assetGuid.ToString() + "," + + String(std::to_string(textureRef.localID).c_str()) + "," + + String(std::to_string(static_cast(textureRef.resourceType)).c_str()); - manager.SetResourceRoot(projectRoot.string().c_str()); + WriteArtifactString(output, "baseColorTexture"); + WriteArtifactString(output, encodedTextureRef); + WriteArtifactString(output, ""); + } MaterialLoader loader; LoadResult result = loader.Load("Library/Manual/test.xcmat"); @@ -572,9 +772,8 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) { auto* material = static_cast(result.resource); ASSERT_NE(material, nullptr); EXPECT_EQ(material->GetTextureBindingCount(), 1u); - EXPECT_EQ( - fs::path(material->GetTextureBindingPath(0).CStr()).lexically_normal().generic_string(), - (projectRoot / "Assets" / "checker.bmp").lexically_normal().generic_string()); + EXPECT_TRUE(material->GetTextureBindingAssetRef(0).IsValid()); + EXPECT_TRUE(material->GetTextureBindingPath(0).Empty()); const ResourceHandle initialTexture = material->GetTexture("baseColorTexture"); EXPECT_FALSE(initialTexture.IsValid()); @@ -585,6 +784,9 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) { ASSERT_TRUE(loadedTexture.IsValid()); EXPECT_EQ(loadedTexture->GetWidth(), 2u); EXPECT_EQ(loadedTexture->GetHeight(), 2u); + EXPECT_EQ( + fs::path(material->GetTextureBindingPath(0).CStr()).lexically_normal().generic_string(), + fs::path("Assets/checker.bmp").lexically_normal().generic_string()); delete material; manager.SetResourceRoot("");