Formalize material schema and constant layout contract
This commit is contained in:
@@ -308,6 +308,16 @@ Unity-like Shader Authoring (.shader)
|
|||||||
- material 仍然缺少基于 shader property 的正式类型校验、默认值回退和资源映射
|
- material 仍然缺少基于 shader property 的正式类型校验、默认值回退和资源映射
|
||||||
- renderer 目前虽然能消费 pass resources,但 material binding 仍偏 builtin-forward 特判
|
- 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 扩展为正式材质执行链路
|
### 阶段 C:把 Pass Binding 扩展为正式材质执行链路
|
||||||
|
|
||||||
目标:
|
目标:
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
#include <XCEngine/Resources/Mesh/Mesh.h>
|
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||||
#include <XCEngine/Rendering/VisibleRenderObject.h>
|
#include <XCEngine/Rendering/VisibleRenderObject.h>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
namespace Rendering {
|
namespace Rendering {
|
||||||
|
|
||||||
@@ -122,6 +124,26 @@ struct BuiltinForwardMaterialData {
|
|||||||
Math::Vector4 baseColorFactor = Math::Vector4::One();
|
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(
|
inline const Resources::ShaderPropertyDesc* FindShaderPropertyBySemantic(
|
||||||
const Resources::Material* material,
|
const Resources::Material* material,
|
||||||
const Containers::String& semantic) {
|
const Containers::String& semantic) {
|
||||||
@@ -220,6 +242,25 @@ inline BuiltinForwardMaterialData BuildBuiltinForwardMaterialData(const Resource
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline MaterialConstantPayloadView ResolveSchemaMaterialConstantPayload(const Resources::Material* material) {
|
||||||
|
if (material == nullptr || material->GetShader() == nullptr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const Containers::Array<Resources::MaterialConstantFieldDesc>& constantLayout = material->GetConstantLayout();
|
||||||
|
const Containers::Array<Core::uint8>& 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(
|
inline const Resources::Material* ResolveMaterial(
|
||||||
const Components::MeshRendererComponent* meshRenderer,
|
const Components::MeshRendererComponent* meshRenderer,
|
||||||
const Resources::Mesh* mesh,
|
const Resources::Mesh* mesh,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <XCEngine/Core/Asset/IResource.h>
|
#include <XCEngine/Core/Asset/IResource.h>
|
||||||
|
#include <XCEngine/Core/Asset/AssetRef.h>
|
||||||
#include <XCEngine/Core/Asset/ResourceHandle.h>
|
#include <XCEngine/Core/Asset/ResourceHandle.h>
|
||||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||||
#include <XCEngine/Resources/Texture/Texture.h>
|
#include <XCEngine/Resources/Texture/Texture.h>
|
||||||
@@ -128,6 +129,14 @@ struct MaterialProperty {
|
|||||||
MaterialProperty() : type(MaterialPropertyType::Float), refCount(0) {}
|
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 {
|
struct MaterialTagEntry {
|
||||||
Containers::String name;
|
Containers::String name;
|
||||||
Containers::String value;
|
Containers::String value;
|
||||||
@@ -143,6 +152,7 @@ struct MaterialTextureBinding {
|
|||||||
Containers::String name;
|
Containers::String name;
|
||||||
Core::uint32 slot = 0;
|
Core::uint32 slot = 0;
|
||||||
ResourceHandle<Texture> texture;
|
ResourceHandle<Texture> texture;
|
||||||
|
AssetRef textureRef;
|
||||||
Containers::String texturePath;
|
Containers::String texturePath;
|
||||||
std::shared_ptr<PendingTextureLoadState> pendingLoad;
|
std::shared_ptr<PendingTextureLoadState> pendingLoad;
|
||||||
};
|
};
|
||||||
@@ -190,6 +200,9 @@ public:
|
|||||||
void SetInt(const Containers::String& name, Core::int32 value);
|
void SetInt(const Containers::String& name, Core::int32 value);
|
||||||
void SetBool(const Containers::String& name, bool value);
|
void SetBool(const Containers::String& name, bool value);
|
||||||
void SetTexture(const Containers::String& name, const ResourceHandle<Texture>& texture);
|
void SetTexture(const Containers::String& name, const ResourceHandle<Texture>& 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);
|
void SetTexturePath(const Containers::String& name, const Containers::String& texturePath);
|
||||||
|
|
||||||
float GetFloat(const Containers::String& name) const;
|
float GetFloat(const Containers::String& name) const;
|
||||||
@@ -201,12 +214,16 @@ public:
|
|||||||
ResourceHandle<Texture> GetTexture(const Containers::String& name) const;
|
ResourceHandle<Texture> GetTexture(const Containers::String& name) const;
|
||||||
Core::uint32 GetTextureBindingCount() const { return static_cast<Core::uint32>(m_textureBindings.Size()); }
|
Core::uint32 GetTextureBindingCount() const { return static_cast<Core::uint32>(m_textureBindings.Size()); }
|
||||||
Containers::String GetTextureBindingName(Core::uint32 index) const;
|
Containers::String GetTextureBindingName(Core::uint32 index) const;
|
||||||
|
AssetRef GetTextureBindingAssetRef(Core::uint32 index) const;
|
||||||
Containers::String GetTextureBindingPath(Core::uint32 index) const;
|
Containers::String GetTextureBindingPath(Core::uint32 index) const;
|
||||||
|
ResourceHandle<Texture> GetTextureBindingLoadedTexture(Core::uint32 index) const;
|
||||||
ResourceHandle<Texture> GetTextureBindingTexture(Core::uint32 index) const;
|
ResourceHandle<Texture> GetTextureBindingTexture(Core::uint32 index) const;
|
||||||
const Containers::Array<MaterialTextureBinding>& GetTextureBindings() const { return m_textureBindings; }
|
const Containers::Array<MaterialTextureBinding>& GetTextureBindings() const { return m_textureBindings; }
|
||||||
std::vector<MaterialProperty> GetProperties() const;
|
std::vector<MaterialProperty> GetProperties() const;
|
||||||
|
|
||||||
const Containers::Array<Core::uint8>& GetConstantBufferData() const { return m_constantBufferData; }
|
const Containers::Array<Core::uint8>& GetConstantBufferData() const { return m_constantBufferData; }
|
||||||
|
const Containers::Array<MaterialConstantFieldDesc>& GetConstantLayout() const { return m_constantLayout; }
|
||||||
|
const MaterialConstantFieldDesc* FindConstantField(const Containers::String& name) const;
|
||||||
void UpdateConstantBuffer();
|
void UpdateConstantBuffer();
|
||||||
Core::uint64 GetChangeVersion() const { return m_changeVersion; }
|
Core::uint64 GetChangeVersion() const { return m_changeVersion; }
|
||||||
void RecalculateMemorySize();
|
void RecalculateMemorySize();
|
||||||
@@ -216,6 +233,10 @@ public:
|
|||||||
void ClearAllProperties();
|
void ClearAllProperties();
|
||||||
|
|
||||||
private:
|
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 BeginAsyncTextureLoad(Core::uint32 index);
|
||||||
void ResolvePendingTextureBinding(Core::uint32 index);
|
void ResolvePendingTextureBinding(Core::uint32 index);
|
||||||
void ResolvePendingTextureBindings();
|
void ResolvePendingTextureBindings();
|
||||||
@@ -228,6 +249,7 @@ private:
|
|||||||
Containers::String m_shaderPass;
|
Containers::String m_shaderPass;
|
||||||
Containers::Array<MaterialTagEntry> m_tags;
|
Containers::Array<MaterialTagEntry> m_tags;
|
||||||
Containers::HashMap<Containers::String, MaterialProperty> m_properties;
|
Containers::HashMap<Containers::String, MaterialProperty> m_properties;
|
||||||
|
Containers::Array<MaterialConstantFieldDesc> m_constantLayout;
|
||||||
Containers::Array<Core::uint8> m_constantBufferData;
|
Containers::Array<Core::uint8> m_constantBufferData;
|
||||||
Containers::Array<MaterialTextureBinding> m_textureBindings;
|
Containers::Array<MaterialTextureBinding> m_textureBindings;
|
||||||
Core::uint64 m_changeVersion = 1;
|
Core::uint64 m_changeVersion = 1;
|
||||||
|
|||||||
@@ -42,11 +42,42 @@ Containers::String ToContainersString(const std::string& value) {
|
|||||||
return Containers::String(value.c_str());
|
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) {
|
Containers::String NormalizeArtifactPathString(const Containers::String& path) {
|
||||||
if (path.Empty()) {
|
if (path.Empty()) {
|
||||||
return Containers::String();
|
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) {
|
bool IsProjectRelativePath(const fs::path& path) {
|
||||||
@@ -321,11 +352,62 @@ std::vector<MaterialProperty> GatherMaterialProperties(const Material& material)
|
|||||||
return material.GetProperties();
|
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<int>(assetRef.resourceType));
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetRef ResolveTextureBindingAssetRef(
|
||||||
|
const Material& material,
|
||||||
|
Core::uint32 bindingIndex,
|
||||||
|
const std::unordered_map<const Texture*, AssetRef>& textureAssetRefs,
|
||||||
|
const AssetDatabase* assetDatabase) {
|
||||||
|
const AssetRef bindingAssetRef = material.GetTextureBindingAssetRef(bindingIndex);
|
||||||
|
if (bindingAssetRef.IsValid()) {
|
||||||
|
return bindingAssetRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResourceHandle<Texture> 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(
|
Containers::String ResolveTextureBindingPath(
|
||||||
const Material& material,
|
const Material& material,
|
||||||
Core::uint32 bindingIndex,
|
Core::uint32 bindingIndex,
|
||||||
const std::unordered_map<const Texture*, Containers::String>& textureArtifactPaths) {
|
const std::unordered_map<const Texture*, Containers::String>& textureArtifactPaths) {
|
||||||
const ResourceHandle<Texture> textureHandle = material.GetTextureBindingTexture(bindingIndex);
|
const ResourceHandle<Texture> textureHandle = material.GetTextureBindingLoadedTexture(bindingIndex);
|
||||||
const Texture* texture = textureHandle.Get();
|
const Texture* texture = textureHandle.Get();
|
||||||
if (texture != nullptr) {
|
if (texture != nullptr) {
|
||||||
const auto textureIt = textureArtifactPaths.find(texture);
|
const auto textureIt = textureArtifactPaths.find(texture);
|
||||||
@@ -344,7 +426,9 @@ Containers::String ResolveTextureBindingPath(
|
|||||||
bool WriteMaterialArtifactFile(
|
bool WriteMaterialArtifactFile(
|
||||||
const fs::path& artifactPath,
|
const fs::path& artifactPath,
|
||||||
const Material& material,
|
const Material& material,
|
||||||
const std::unordered_map<const Texture*, Containers::String>& textureArtifactPaths = {}) {
|
const std::unordered_map<const Texture*, Containers::String>& textureArtifactPaths = {},
|
||||||
|
const std::unordered_map<const Texture*, AssetRef>& textureAssetRefs = {},
|
||||||
|
const AssetDatabase* assetDatabase = nullptr) {
|
||||||
std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc);
|
std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc);
|
||||||
if (!output.is_open()) {
|
if (!output.is_open()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -398,8 +482,11 @@ bool WriteMaterialArtifactFile(
|
|||||||
|
|
||||||
for (Core::uint32 bindingIndex = 0; bindingIndex < material.GetTextureBindingCount(); ++bindingIndex) {
|
for (Core::uint32 bindingIndex = 0; bindingIndex < material.GetTextureBindingCount(); ++bindingIndex) {
|
||||||
const Containers::String bindingName = material.GetTextureBindingName(bindingIndex);
|
const Containers::String bindingName = material.GetTextureBindingName(bindingIndex);
|
||||||
|
const AssetRef textureAssetRef =
|
||||||
|
ResolveTextureBindingAssetRef(material, bindingIndex, textureAssetRefs, assetDatabase);
|
||||||
|
|
||||||
WriteString(output, bindingName);
|
WriteString(output, bindingName);
|
||||||
|
WriteString(output, Containers::String(EncodeAssetRef(textureAssetRef).c_str()));
|
||||||
WriteString(output, ResolveTextureBindingPath(material, bindingIndex, textureArtifactPaths));
|
WriteString(output, ResolveTextureBindingPath(material, bindingIndex, textureArtifactPaths));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,7 +651,6 @@ void AssetDatabase::Initialize(const Containers::String& projectRoot) {
|
|||||||
LoadSourceAssetDB();
|
LoadSourceAssetDB();
|
||||||
LoadArtifactDB();
|
LoadArtifactDB();
|
||||||
ScanAssets();
|
ScanAssets();
|
||||||
SaveArtifactDB();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AssetDatabase::Shutdown() {
|
void AssetDatabase::Shutdown() {
|
||||||
@@ -581,8 +667,8 @@ void AssetDatabase::Shutdown() {
|
|||||||
m_artifactsByGuid.clear();
|
m_artifactsByGuid.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AssetDatabase::Refresh() {
|
AssetDatabase::MaintenanceStats AssetDatabase::Refresh() {
|
||||||
ScanAssets();
|
return ScanAssets();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AssetDatabase::ResolvePath(const Containers::String& requestPath,
|
bool AssetDatabase::ResolvePath(const Containers::String& requestPath,
|
||||||
@@ -644,6 +730,25 @@ bool AssetDatabase::TryGetAssetGuid(const Containers::String& requestPath, Asset
|
|||||||
return outGuid.IsValid();
|
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,
|
bool AssetDatabase::TryGetAssetRef(const Containers::String& requestPath,
|
||||||
ResourceType resourceType,
|
ResourceType resourceType,
|
||||||
AssetRef& outRef) const {
|
AssetRef& outRef) const {
|
||||||
@@ -658,6 +763,115 @@ bool AssetDatabase::TryGetAssetRef(const Containers::String& requestPath,
|
|||||||
return true;
|
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<SourceAssetRecord> 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 {
|
bool AssetDatabase::TryGetPrimaryAssetPath(const AssetGUID& guid, Containers::String& outRelativePath) const {
|
||||||
const auto sourceIt = m_sourcesByGuid.find(guid);
|
const auto sourceIt = m_sourcesByGuid.find(guid);
|
||||||
if (sourceIt == m_sourcesByGuid.end()) {
|
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<std::string, bool> seenPaths;
|
std::unordered_map<std::string, bool> seenPaths;
|
||||||
const fs::path assetsRootPath(m_assetsRoot.CStr());
|
const fs::path assetsRootPath(m_assetsRoot.CStr());
|
||||||
if (fs::exists(assetsRootPath)) {
|
if (fs::exists(assetsRootPath)) {
|
||||||
ScanAssetPath(assetsRootPath, seenPaths);
|
ScanAssetPath(assetsRootPath, seenPaths);
|
||||||
}
|
}
|
||||||
RemoveMissingRecords(seenPaths);
|
RemoveMissingRecords(seenPaths);
|
||||||
|
stats.removedArtifactCount = CleanupOrphanedArtifacts();
|
||||||
SaveSourceAssetDB();
|
SaveSourceAssetDB();
|
||||||
|
SaveArtifactDB();
|
||||||
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AssetDatabase::ScanAssetPath(const fs::path& path,
|
void AssetDatabase::ScanAssetPath(const fs::path& path,
|
||||||
@@ -902,9 +1120,75 @@ void AssetDatabase::RemoveMissingRecords(const std::unordered_map<std::string, b
|
|||||||
m_sourcesByPathKey.erase(recordIt);
|
m_sourcesByPathKey.erase(recordIt);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!missingPathKeys.empty()) {
|
}
|
||||||
SaveArtifactDB();
|
|
||||||
|
Core::uint32 AssetDatabase::CleanupOrphanedArtifacts() const {
|
||||||
|
std::error_code ec;
|
||||||
|
const fs::path artifactsRoot = fs::path(m_libraryRoot.CStr()) / "Artifacts";
|
||||||
|
if (!fs::exists(artifactsRoot, ec) || !fs::is_directory(artifactsRoot, ec)) {
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unordered_set<std::string> 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,
|
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;
|
m_sourcesByPathKey[ToStdString(MakeKey(sourceRecord.relativePath))].lastKnownArtifactKey = rebuiltRecord.artifactKey;
|
||||||
SaveArtifactDB();
|
SaveArtifactDB();
|
||||||
SaveSourceAssetDB();
|
SaveSourceAssetDB();
|
||||||
|
CleanupOrphanedArtifacts();
|
||||||
artifactRecord = &m_artifactsByGuid[sourceRecord.guid];
|
artifactRecord = &m_artifactsByGuid[sourceRecord.guid];
|
||||||
|
outAsset.imported = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (artifactRecord == nullptr) {
|
if (artifactRecord == nullptr) {
|
||||||
@@ -1236,14 +1522,7 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath,
|
|||||||
}
|
}
|
||||||
|
|
||||||
outAsset.exists = true;
|
outAsset.exists = true;
|
||||||
outAsset.artifactReady = true;
|
PopulateResolvedAssetResult(m_projectRoot, sourceRecord, *artifactRecord, outAsset.imported, outAsset);
|
||||||
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;
|
|
||||||
|
|
||||||
if (ShouldTraceAssetPath(requestPath)) {
|
if (ShouldTraceAssetPath(requestPath)) {
|
||||||
Debug::Logger::Get().Info(
|
Debug::Logger::Get().Info(
|
||||||
@@ -1331,7 +1610,7 @@ bool AssetDatabase::ImportMaterialAsset(const SourceAssetRecord& sourceRecord,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bool writeOk =
|
const bool writeOk =
|
||||||
WriteMaterialArtifactFile(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *material);
|
WriteMaterialArtifactFile(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(), *material, {}, {}, this);
|
||||||
delete material;
|
delete material;
|
||||||
if (!writeOk) {
|
if (!writeOk) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1380,6 +1659,7 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
|||||||
|
|
||||||
bool writeOk = true;
|
bool writeOk = true;
|
||||||
std::unordered_map<const Texture*, Containers::String> textureArtifactPaths;
|
std::unordered_map<const Texture*, Containers::String> textureArtifactPaths;
|
||||||
|
std::unordered_map<const Texture*, AssetRef> textureAssetRefs;
|
||||||
for (size_t textureIndex = 0; writeOk && textureIndex < mesh->GetTextures().Size(); ++textureIndex) {
|
for (size_t textureIndex = 0; writeOk && textureIndex < mesh->GetTextures().Size(); ++textureIndex) {
|
||||||
Texture* texture = mesh->GetTextures()[textureIndex];
|
Texture* texture = mesh->GetTextures()[textureIndex];
|
||||||
if (texture == nullptr) {
|
if (texture == nullptr) {
|
||||||
@@ -1396,6 +1676,13 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
|||||||
}
|
}
|
||||||
|
|
||||||
textureArtifactPaths.emplace(texture, textureArtifactPath);
|
textureArtifactPaths.emplace(texture, textureArtifactPath);
|
||||||
|
|
||||||
|
if (!texture->GetPath().Empty()) {
|
||||||
|
AssetRef textureAssetRef;
|
||||||
|
if (TryGetAssetRef(texture->GetPath(), ResourceType::Texture, textureAssetRef)) {
|
||||||
|
textureAssetRefs.emplace(texture, textureAssetRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Containers::String> materialArtifactPaths;
|
std::vector<Containers::String> materialArtifactPaths;
|
||||||
@@ -1412,7 +1699,9 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
|||||||
writeOk = WriteMaterialArtifactFile(
|
writeOk = WriteMaterialArtifactFile(
|
||||||
fs::path(m_projectRoot.CStr()) / materialArtifactPath.CStr(),
|
fs::path(m_projectRoot.CStr()) / materialArtifactPath.CStr(),
|
||||||
*material,
|
*material,
|
||||||
textureArtifactPaths);
|
textureArtifactPaths,
|
||||||
|
textureAssetRefs,
|
||||||
|
this);
|
||||||
if (!writeOk) {
|
if (!writeOk) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -888,7 +888,7 @@ BuiltinForwardPipeline::CachedDescriptorSet* BuiltinForwardPipeline::GetOrCreate
|
|||||||
Core::uint32 setIndex,
|
Core::uint32 setIndex,
|
||||||
Core::uint64 objectId,
|
Core::uint64 objectId,
|
||||||
const Resources::Material* material,
|
const Resources::Material* material,
|
||||||
const PerMaterialConstants& materialConstants,
|
const MaterialConstantPayloadView& materialConstants,
|
||||||
RHI::RHIResourceView* textureView) {
|
RHI::RHIResourceView* textureView) {
|
||||||
DynamicDescriptorSetKey key = {};
|
DynamicDescriptorSetKey key = {};
|
||||||
key.passLayout = passLayoutKey;
|
key.passLayout = passLayoutKey;
|
||||||
@@ -911,10 +911,13 @@ BuiltinForwardPipeline::CachedDescriptorSet* BuiltinForwardPipeline::GetOrCreate
|
|||||||
if (!passLayout.material.IsValid() || passLayout.material.set != setIndex) {
|
if (!passLayout.material.IsValid() || passLayout.material.set != setIndex) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
if (!materialConstants.IsValid()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
cachedDescriptorSet.descriptorSet.set->WriteConstant(
|
cachedDescriptorSet.descriptorSet.set->WriteConstant(
|
||||||
passLayout.material.binding,
|
passLayout.material.binding,
|
||||||
&materialConstants,
|
materialConstants.data,
|
||||||
sizeof(materialConstants));
|
materialConstants.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setLayout.usesTexture) {
|
if (setLayout.usesTexture) {
|
||||||
@@ -1046,10 +1049,26 @@ bool BuiltinForwardPipeline::DrawVisibleItem(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BuiltinForwardMaterialData materialData = BuildBuiltinForwardMaterialData(material);
|
MaterialConstantPayloadView materialConstants = ResolveSchemaMaterialConstantPayload(material);
|
||||||
const PerMaterialConstants materialConstants = {
|
FallbackPerMaterialConstants fallbackMaterialConstants = {};
|
||||||
materialData.baseColorFactor
|
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) {
|
if (passLayout->descriptorSetCount > 0) {
|
||||||
std::vector<RHI::RHIDescriptorSet*> descriptorSets(passLayout->descriptorSetCount, nullptr);
|
std::vector<RHI::RHIDescriptorSet*> descriptorSets(passLayout->descriptorSetCount, nullptr);
|
||||||
|
|||||||
@@ -3,8 +3,11 @@
|
|||||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
@@ -14,6 +17,10 @@ namespace {
|
|||||||
|
|
||||||
constexpr size_t kMaterialConstantSlotSize = 16;
|
constexpr size_t kMaterialConstantSlotSize = 16;
|
||||||
|
|
||||||
|
bool HasVirtualPathScheme(const Containers::String& path) {
|
||||||
|
return std::string(path.CStr()).find("://") != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
bool IsPackedMaterialPropertyType(MaterialPropertyType type) {
|
bool IsPackedMaterialPropertyType(MaterialPropertyType type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MaterialPropertyType::Float:
|
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(
|
void RemoveTextureBindingByName(
|
||||||
Containers::Array<MaterialTextureBinding>& textureBindings,
|
Containers::Array<MaterialTextureBinding>& textureBindings,
|
||||||
const Containers::String& name) {
|
const Containers::String& name) {
|
||||||
@@ -53,14 +82,187 @@ void RemoveTextureBindingByName(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EnsureTextureProperty(Containers::HashMap<Containers::String, MaterialProperty>& properties,
|
void EnsureTextureProperty(Containers::HashMap<Containers::String, MaterialProperty>& properties,
|
||||||
const Containers::String& name) {
|
const Containers::String& name,
|
||||||
|
MaterialPropertyType type = MaterialPropertyType::Texture) {
|
||||||
MaterialProperty prop;
|
MaterialProperty prop;
|
||||||
prop.name = name;
|
prop.name = name;
|
||||||
prop.type = MaterialPropertyType::Texture;
|
prop.type = type;
|
||||||
prop.refCount = 1;
|
prop.refCount = 1;
|
||||||
properties.Insert(name, prop);
|
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<unsigned char>(*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<unsigned char>(*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<unsigned char>(*endPtr)) == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
++endPtr;
|
||||||
|
}
|
||||||
|
|
||||||
|
outValue = static_cast<Core::int32>(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) {
|
void WritePackedMaterialProperty(Core::uint8* destination, const MaterialProperty& property) {
|
||||||
std::memset(destination, 0, kMaterialConstantSlotSize);
|
std::memset(destination, 0, kMaterialConstantSlotSize);
|
||||||
|
|
||||||
@@ -114,6 +316,7 @@ void Material::Release() {
|
|||||||
m_shaderPass.Clear();
|
m_shaderPass.Clear();
|
||||||
m_tags.Clear();
|
m_tags.Clear();
|
||||||
m_properties.Clear();
|
m_properties.Clear();
|
||||||
|
m_constantLayout.Clear();
|
||||||
m_textureBindings.Clear();
|
m_textureBindings.Clear();
|
||||||
m_constantBufferData.Clear();
|
m_constantBufferData.Clear();
|
||||||
m_changeVersion = 1;
|
m_changeVersion = 1;
|
||||||
@@ -123,7 +326,8 @@ void Material::Release() {
|
|||||||
|
|
||||||
void Material::SetShader(const ResourceHandle<Shader>& shader) {
|
void Material::SetShader(const ResourceHandle<Shader>& shader) {
|
||||||
m_shader = shader;
|
m_shader = shader;
|
||||||
MarkChanged(false);
|
SyncShaderSchemaProperties(true);
|
||||||
|
MarkChanged(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Material::SetRenderQueue(Core::int32 renderQueue) {
|
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) {
|
void Material::SetFloat(const Containers::String& name, float value) {
|
||||||
|
if (!CanAssignPropertyType(name, MaterialPropertyType::Float)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
RemoveTextureBindingByName(m_textureBindings, name);
|
RemoveTextureBindingByName(m_textureBindings, name);
|
||||||
MaterialProperty prop;
|
MaterialProperty prop;
|
||||||
prop.name = name;
|
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) {
|
void Material::SetFloat2(const Containers::String& name, const Math::Vector2& value) {
|
||||||
|
if (!CanAssignPropertyType(name, MaterialPropertyType::Float2)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
RemoveTextureBindingByName(m_textureBindings, name);
|
RemoveTextureBindingByName(m_textureBindings, name);
|
||||||
MaterialProperty prop;
|
MaterialProperty prop;
|
||||||
prop.name = name;
|
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) {
|
void Material::SetFloat3(const Containers::String& name, const Math::Vector3& value) {
|
||||||
|
if (!CanAssignPropertyType(name, MaterialPropertyType::Float3)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
RemoveTextureBindingByName(m_textureBindings, name);
|
RemoveTextureBindingByName(m_textureBindings, name);
|
||||||
MaterialProperty prop;
|
MaterialProperty prop;
|
||||||
prop.name = name;
|
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) {
|
void Material::SetFloat4(const Containers::String& name, const Math::Vector4& value) {
|
||||||
|
if (!CanAssignPropertyType(name, MaterialPropertyType::Float4)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
RemoveTextureBindingByName(m_textureBindings, name);
|
RemoveTextureBindingByName(m_textureBindings, name);
|
||||||
MaterialProperty prop;
|
MaterialProperty prop;
|
||||||
prop.name = name;
|
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) {
|
void Material::SetInt(const Containers::String& name, Core::int32 value) {
|
||||||
|
if (!CanAssignPropertyType(name, MaterialPropertyType::Int)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
RemoveTextureBindingByName(m_textureBindings, name);
|
RemoveTextureBindingByName(m_textureBindings, name);
|
||||||
MaterialProperty prop;
|
MaterialProperty prop;
|
||||||
prop.name = name;
|
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) {
|
void Material::SetBool(const Containers::String& name, bool value) {
|
||||||
|
if (!CanAssignPropertyType(name, MaterialPropertyType::Bool)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
RemoveTextureBindingByName(m_textureBindings, name);
|
RemoveTextureBindingByName(m_textureBindings, name);
|
||||||
MaterialProperty prop;
|
MaterialProperty prop;
|
||||||
prop.name = name;
|
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>& texture) {
|
void Material::SetTexture(const Containers::String& name, const ResourceHandle<Texture>& 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) {
|
for (auto& binding : m_textureBindings) {
|
||||||
if (binding.name == name) {
|
if (binding.name == name) {
|
||||||
binding.texture = texture;
|
binding.texture = texture;
|
||||||
binding.texturePath = texture.Get() != nullptr ? texture->GetPath() : Containers::String();
|
binding.textureRef = textureRef;
|
||||||
binding.pendingLoad.reset();
|
|
||||||
MarkChanged(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialTextureBinding binding;
|
|
||||||
binding.name = name;
|
|
||||||
binding.slot = static_cast<Core::uint32>(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.texturePath = texturePath;
|
binding.texturePath = texturePath;
|
||||||
binding.pendingLoad.reset();
|
binding.pendingLoad.reset();
|
||||||
MarkChanged(false);
|
MarkChanged(false);
|
||||||
@@ -322,6 +539,84 @@ void Material::SetTexturePath(const Containers::String& name, const Containers::
|
|||||||
MaterialTextureBinding binding;
|
MaterialTextureBinding binding;
|
||||||
binding.name = name;
|
binding.name = name;
|
||||||
binding.slot = static_cast<Core::uint32>(m_textureBindings.Size());
|
binding.slot = static_cast<Core::uint32>(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<Core::uint32>(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<Core::uint32>(m_textureBindings.Size());
|
||||||
|
binding.textureRef.Reset();
|
||||||
binding.texturePath = texturePath;
|
binding.texturePath = texturePath;
|
||||||
m_textureBindings.PushBack(binding);
|
m_textureBindings.PushBack(binding);
|
||||||
MarkChanged(false);
|
MarkChanged(false);
|
||||||
@@ -384,7 +679,7 @@ ResourceHandle<Texture> Material::GetTexture(const Containers::String& name) con
|
|||||||
if (binding.name == name) {
|
if (binding.name == name) {
|
||||||
if (binding.texture.Get() == nullptr &&
|
if (binding.texture.Get() == nullptr &&
|
||||||
binding.pendingLoad == nullptr &&
|
binding.pendingLoad == nullptr &&
|
||||||
!binding.texturePath.Empty()) {
|
(!binding.texturePath.Empty() || binding.textureRef.IsValid())) {
|
||||||
material->BeginAsyncTextureLoad(bindingIndex);
|
material->BeginAsyncTextureLoad(bindingIndex);
|
||||||
}
|
}
|
||||||
return binding.texture;
|
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();
|
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 {
|
Containers::String Material::GetTextureBindingPath(Core::uint32 index) const {
|
||||||
return index < m_textureBindings.Size() ? m_textureBindings[index].texturePath : Containers::String();
|
return index < m_textureBindings.Size() ? m_textureBindings[index].texturePath : Containers::String();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ResourceHandle<Texture> Material::GetTextureBindingLoadedTexture(Core::uint32 index) const {
|
||||||
|
return index < m_textureBindings.Size() ? m_textureBindings[index].texture : ResourceHandle<Texture>();
|
||||||
|
}
|
||||||
|
|
||||||
ResourceHandle<Texture> Material::GetTextureBindingTexture(Core::uint32 index) const {
|
ResourceHandle<Texture> Material::GetTextureBindingTexture(Core::uint32 index) const {
|
||||||
Material* material = const_cast<Material*>(this);
|
Material* material = const_cast<Material*>(this);
|
||||||
material->ResolvePendingTextureBinding(index);
|
material->ResolvePendingTextureBinding(index);
|
||||||
@@ -408,7 +711,7 @@ ResourceHandle<Texture> Material::GetTextureBindingTexture(Core::uint32 index) c
|
|||||||
MaterialTextureBinding& binding = material->m_textureBindings[index];
|
MaterialTextureBinding& binding = material->m_textureBindings[index];
|
||||||
if (binding.texture.Get() == nullptr &&
|
if (binding.texture.Get() == nullptr &&
|
||||||
binding.pendingLoad == nullptr &&
|
binding.pendingLoad == nullptr &&
|
||||||
!binding.texturePath.Empty()) {
|
(!binding.texturePath.Empty() || binding.textureRef.IsValid())) {
|
||||||
material->BeginAsyncTextureLoad(index);
|
material->BeginAsyncTextureLoad(index);
|
||||||
}
|
}
|
||||||
return binding.texture;
|
return binding.texture;
|
||||||
@@ -427,25 +730,63 @@ std::vector<MaterialProperty> Material::GetProperties() const {
|
|||||||
return properties;
|
return properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Material::UpdateConstantBuffer() {
|
const MaterialConstantFieldDesc* Material::FindConstantField(const Containers::String& name) const {
|
||||||
std::vector<const MaterialProperty*> packedProperties;
|
for (const MaterialConstantFieldDesc& field : m_constantLayout) {
|
||||||
const auto pairs = m_properties.GetPairs();
|
if (field.name == name) {
|
||||||
packedProperties.reserve(pairs.Size());
|
return &field;
|
||||||
for (const auto& pair : pairs) {
|
|
||||||
if (IsPackedMaterialPropertyType(pair.second.type)) {
|
|
||||||
packedProperties.push_back(&pair.second);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::sort(
|
return nullptr;
|
||||||
packedProperties.begin(),
|
}
|
||||||
packedProperties.end(),
|
|
||||||
[](const MaterialProperty* left, const MaterialProperty* right) {
|
void Material::UpdateConstantBuffer() {
|
||||||
return std::strcmp(left->name.CStr(), right->name.CStr()) < 0;
|
std::vector<MaterialProperty> 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<Core::uint32>(kMaterialConstantSlotSize);
|
||||||
|
m_constantLayout.PushBack(field);
|
||||||
|
currentOffset += field.alignedSize;
|
||||||
|
}
|
||||||
|
|
||||||
m_constantBufferData.Clear();
|
m_constantBufferData.Clear();
|
||||||
m_constantBufferData.Resize(packedProperties.size() * kMaterialConstantSlotSize);
|
m_constantBufferData.Resize(static_cast<size_t>(currentOffset));
|
||||||
if (!packedProperties.empty()) {
|
if (!packedProperties.empty()) {
|
||||||
std::memset(m_constantBufferData.Data(), 0, m_constantBufferData.Size());
|
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) {
|
for (size_t propertyIndex = 0; propertyIndex < packedProperties.size(); ++propertyIndex) {
|
||||||
WritePackedMaterialProperty(
|
WritePackedMaterialProperty(
|
||||||
m_constantBufferData.Data() + propertyIndex * kMaterialConstantSlotSize,
|
m_constantBufferData.Data() + propertyIndex * kMaterialConstantSlotSize,
|
||||||
*packedProperties[propertyIndex]);
|
packedProperties[propertyIndex]);
|
||||||
}
|
}
|
||||||
UpdateMemorySize();
|
UpdateMemorySize();
|
||||||
}
|
}
|
||||||
@@ -468,7 +809,15 @@ void Material::BeginAsyncTextureLoad(Core::uint32 index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MaterialTextureBinding& binding = m_textureBindings[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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,7 +854,9 @@ void Material::ResolvePendingTextureBinding(Core::uint32 index) {
|
|||||||
|
|
||||||
binding.texture = ResourceHandle<Texture>(static_cast<Texture*>(completedLoad->resource));
|
binding.texture = ResourceHandle<Texture>(static_cast<Texture*>(completedLoad->resource));
|
||||||
if (binding.texture.Get() != nullptr) {
|
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) {
|
void Material::RemoveProperty(const Containers::String& name) {
|
||||||
|
if (ResetPropertyToShaderDefault(name)) {
|
||||||
|
MarkChanged(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const MaterialProperty* property = m_properties.Find(name);
|
const MaterialProperty* property = m_properties.Find(name);
|
||||||
const bool removeTextureBinding =
|
const bool removeTextureBinding =
|
||||||
property != nullptr &&
|
property != nullptr &&
|
||||||
@@ -536,9 +892,79 @@ void Material::RemoveProperty(const Containers::String& name) {
|
|||||||
|
|
||||||
void Material::ClearAllProperties() {
|
void Material::ClearAllProperties() {
|
||||||
m_properties.Clear();
|
m_properties.Clear();
|
||||||
|
m_constantLayout.Clear();
|
||||||
m_textureBindings.Clear();
|
m_textureBindings.Clear();
|
||||||
m_constantBufferData.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<Containers::String> 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) {
|
void Material::MarkChanged(bool updateConstantBuffer) {
|
||||||
@@ -553,6 +979,7 @@ void Material::MarkChanged(bool updateConstantBuffer) {
|
|||||||
|
|
||||||
void Material::UpdateMemorySize() {
|
void Material::UpdateMemorySize() {
|
||||||
m_memorySize = m_constantBufferData.Size() +
|
m_memorySize = m_constantBufferData.Size() +
|
||||||
|
m_constantLayout.Size() * sizeof(MaterialConstantFieldDesc) +
|
||||||
sizeof(MaterialRenderState) +
|
sizeof(MaterialRenderState) +
|
||||||
m_shaderPass.Length() +
|
m_shaderPass.Length() +
|
||||||
m_tags.Size() * sizeof(MaterialTagEntry) +
|
m_tags.Size() * sizeof(MaterialTagEntry) +
|
||||||
@@ -566,6 +993,10 @@ void Material::UpdateMemorySize() {
|
|||||||
m_memorySize += tag.value.Length();
|
m_memorySize += tag.value.Length();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const MaterialConstantFieldDesc& field : m_constantLayout) {
|
||||||
|
m_memorySize += field.name.Length();
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto& binding : m_textureBindings) {
|
for (const auto& binding : m_textureBindings) {
|
||||||
m_memorySize += binding.name.Length();
|
m_memorySize += binding.name.Length();
|
||||||
m_memorySize += binding.texturePath.Length();
|
m_memorySize += binding.texturePath.Length();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
namespace Resources {
|
namespace Resources {
|
||||||
@@ -284,6 +285,21 @@ bool TryParseBoolValue(const std::string& json, const char* key, bool& outValue)
|
|||||||
return false;
|
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<LocalID>(std::stoull(text.substr(firstComma + 1, secondComma - firstComma - 1)));
|
||||||
|
outRef.resourceType = static_cast<ResourceType>(std::stoi(text.substr(secondComma + 1)));
|
||||||
|
return outRef.IsValid();
|
||||||
|
}
|
||||||
|
|
||||||
bool TryExtractObject(const std::string& json, const char* key, std::string& outObject) {
|
bool TryExtractObject(const std::string& json, const char* key, std::string& outObject) {
|
||||||
size_t valuePos = 0;
|
size_t valuePos = 0;
|
||||||
if (!FindValueStart(json, key, valuePos) || valuePos >= json.size() || json[valuePos] != '{') {
|
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;
|
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<unsigned char>(text[last - 1])) != 0) {
|
||||||
|
--last;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.substr(first, last - first);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsJsonValueTerminator(char ch) {
|
||||||
|
return std::isspace(static_cast<unsigned char>(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<unsigned char>(ch)) != 0) {
|
||||||
|
size_t endPos = valuePos + 1;
|
||||||
|
while (endPos < text.size()) {
|
||||||
|
const char current = text[endPos];
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(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<unsigned char>(*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<unsigned char>(*endPtr)) == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
++endPtr;
|
||||||
|
}
|
||||||
|
|
||||||
|
outValue = static_cast<Core::int32>(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<unsigned char>(*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<unsigned char>(*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) {
|
bool TryParseRenderQueueName(const Containers::String& queueName, Core::int32& outQueue) {
|
||||||
const Containers::String normalized = queueName.ToLower();
|
const Containers::String normalized = queueName.ToLower();
|
||||||
if (normalized == "background") {
|
if (normalized == "background") {
|
||||||
@@ -912,7 +1383,7 @@ LoadResult LoadMaterialArtifact(const Containers::String& path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::string magic(fileHeader.magic, fileHeader.magic + 7);
|
const std::string magic(fileHeader.magic, fileHeader.magic + 7);
|
||||||
if (magic != "XCMAT01") {
|
if (magic != "XCMAT02") {
|
||||||
return LoadResult("Invalid material artifact magic: " + path);
|
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) {
|
for (Core::uint32 bindingIndex = 0; bindingIndex < header.textureBindingCount; ++bindingIndex) {
|
||||||
Containers::String bindingName;
|
Containers::String bindingName;
|
||||||
|
Containers::String textureRefText;
|
||||||
Containers::String texturePath;
|
Containers::String texturePath;
|
||||||
if (!ReadMaterialArtifactString(data, offset, bindingName) ||
|
if (!ReadMaterialArtifactString(data, offset, bindingName) ||
|
||||||
|
!ReadMaterialArtifactString(data, offset, textureRefText) ||
|
||||||
!ReadMaterialArtifactString(data, offset, texturePath)) {
|
!ReadMaterialArtifactString(data, offset, texturePath)) {
|
||||||
return LoadResult("Failed to read material artifact texture bindings: " + path);
|
return LoadResult("Failed to read material artifact texture bindings: " + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!texturePath.Empty()) {
|
AssetRef textureRef;
|
||||||
material->SetTexturePath(bindingName, ResolveArtifactDependencyPath(texturePath, path));
|
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<Core::uint8>& dat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (HasKey(jsonText, "properties")) {
|
||||||
|
std::string propertiesObject;
|
||||||
|
if (!TryExtractObject(jsonText, "properties", propertiesObject) ||
|
||||||
|
!TryParseMaterialPropertiesObject(propertiesObject, material)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!TryParseMaterialTextureBindings(jsonText, material)) {
|
if (!TryParseMaterialTextureBindings(jsonText, material)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -479,6 +479,55 @@ TEST(RenderMaterialUtility_Test, ResolvesBuiltinForwardMaterialContractFromShade
|
|||||||
EXPECT_EQ(ResolveBuiltinBaseColorTexture(&material), texture);
|
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>(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>(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<const float*>(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) {
|
TEST(RenderMaterialUtility_Test, UsesOpacityOnlyWhenBaseColorFactorIsMissing) {
|
||||||
Material material;
|
Material material;
|
||||||
material.SetFloat("opacity", 0.35f);
|
material.SetFloat("opacity", 0.35f);
|
||||||
|
|||||||
@@ -12,6 +12,35 @@ using namespace XCEngine::Math;
|
|||||||
|
|
||||||
namespace {
|
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) {
|
TEST(Material, DefaultConstructor) {
|
||||||
Material material;
|
Material material;
|
||||||
EXPECT_EQ(material.GetType(), ResourceType::Material);
|
EXPECT_EQ(material.GetType(), ResourceType::Material);
|
||||||
@@ -225,6 +254,28 @@ TEST(Material, SetTextureReplacesExistingBinding) {
|
|||||||
EXPECT_EQ(material.GetTexture("uDiffuse").Get(), secondTexture);
|
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) {
|
TEST(Material, ChangeVersionIncrementsWhenMaterialMutates) {
|
||||||
Material material;
|
Material material;
|
||||||
const XCEngine::Core::uint64 initialVersion = material.GetChangeVersion();
|
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.SetFloat4("beta", Vector4(1.0f, 2.0f, 3.0f, 4.0f));
|
||||||
material.SetInt("gamma", 7);
|
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();
|
const auto& constantBufferData = material.GetConstantBufferData();
|
||||||
ASSERT_EQ(constantBufferData.Size(), 48u);
|
ASSERT_EQ(constantBufferData.Size(), 48u);
|
||||||
|
|
||||||
@@ -331,4 +400,138 @@ TEST(Material, ClearAllProperties) {
|
|||||||
EXPECT_FALSE(material.HasProperty("uIndex"));
|
EXPECT_FALSE(material.HasProperty("uIndex"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Material, SetShaderSeedsDefaultsAndRemovePropertyRestoresShaderDefault) {
|
||||||
|
Material material;
|
||||||
|
Shader* shader = CreateMaterialSchemaShader();
|
||||||
|
|
||||||
|
material.SetShader(ResourceHandle<Shader>(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>(shader));
|
||||||
|
material.SetFloat("_Metallic", 0.15f);
|
||||||
|
material.SetTexture("_MainTex", ResourceHandle<Texture>(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>(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<Shader>(shaderA));
|
||||||
|
material.SetFloat("Shared", 5.0f);
|
||||||
|
material.SetFloat("OnlyA", 8.0f);
|
||||||
|
|
||||||
|
material.SetShader(ResourceHandle<Shader>(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>(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<const float*>(constantBufferData.Data());
|
||||||
|
const float* secondSlot = reinterpret_cast<const float*>(constantBufferData.Data() + 16);
|
||||||
|
EXPECT_FLOAT_EQ(firstSlot[0], 20.0f);
|
||||||
|
EXPECT_FLOAT_EQ(secondSlot[0], 10.0f);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -66,6 +66,66 @@ void WriteTextFile(const std::filesystem::path& path, const std::string& content
|
|||||||
ASSERT_TRUE(static_cast<bool>(output));
|
ASSERT_TRUE(static_cast<bool>(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<bool>(manifest));
|
||||||
|
|
||||||
|
return manifestPath;
|
||||||
|
}
|
||||||
|
|
||||||
TEST(MaterialLoader, GetResourceType) {
|
TEST(MaterialLoader, GetResourceType) {
|
||||||
MaterialLoader loader;
|
MaterialLoader loader;
|
||||||
EXPECT_EQ(loader.GetResourceType(), ResourceType::Material);
|
EXPECT_EQ(loader.GetResourceType(), ResourceType::Material);
|
||||||
@@ -235,6 +295,135 @@ TEST(MaterialLoader, LoadMaterialWithShaderManifestResolvesShaderPass) {
|
|||||||
fs::remove_all(shaderRoot);
|
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<Material*>(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<Material*>(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) {
|
TEST(MaterialLoader, RejectsUnknownRenderQueueName) {
|
||||||
const std::filesystem::path materialPath =
|
const std::filesystem::path materialPath =
|
||||||
std::filesystem::current_path() / "material_loader_invalid_queue.material";
|
std::filesystem::current_path() / "material_loader_invalid_queue.material";
|
||||||
@@ -380,6 +569,7 @@ TEST(MaterialLoader, ResourceManagerLoadsProjectMaterialTextureAsLazyDependency)
|
|||||||
ASSERT_TRUE(materialHandle.IsValid());
|
ASSERT_TRUE(materialHandle.IsValid());
|
||||||
ASSERT_EQ(materialHandle->GetTextureBindingCount(), 1u);
|
ASSERT_EQ(materialHandle->GetTextureBindingCount(), 1u);
|
||||||
EXPECT_EQ(materialHandle->GetTextureBindingName(0), "baseColorTexture");
|
EXPECT_EQ(materialHandle->GetTextureBindingName(0), "baseColorTexture");
|
||||||
|
EXPECT_TRUE(materialHandle->GetTextureBindingAssetRef(0).IsValid());
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
fs::path(materialHandle->GetTextureBindingPath(0).CStr()).lexically_normal().generic_string(),
|
fs::path(materialHandle->GetTextureBindingPath(0).CStr()).lexically_normal().generic_string(),
|
||||||
(projectRoot / "Assets" / "checker.bmp").lexically_normal().generic_string());
|
(projectRoot / "Assets" / "checker.bmp").lexically_normal().generic_string());
|
||||||
@@ -542,6 +732,9 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) {
|
|||||||
assetsDir / "checker.bmp",
|
assetsDir / "checker.bmp",
|
||||||
fs::copy_options::overwrite_existing);
|
fs::copy_options::overwrite_existing);
|
||||||
|
|
||||||
|
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||||
|
manager.RefreshProjectAssets();
|
||||||
|
|
||||||
{
|
{
|
||||||
std::ofstream output(materialArtifactPath, std::ios::binary | std::ios::trunc);
|
std::ofstream output(materialArtifactPath, std::ios::binary | std::ios::trunc);
|
||||||
ASSERT_TRUE(output.is_open());
|
ASSERT_TRUE(output.is_open());
|
||||||
@@ -558,11 +751,18 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) {
|
|||||||
header.textureBindingCount = 1;
|
header.textureBindingCount = 1;
|
||||||
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||||
|
|
||||||
WriteArtifactString(output, "baseColorTexture");
|
AssetRef textureRef;
|
||||||
WriteArtifactString(output, "Assets/checker.bmp");
|
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<int>(textureRef.resourceType)).c_str());
|
||||||
|
|
||||||
manager.SetResourceRoot(projectRoot.string().c_str());
|
WriteArtifactString(output, "baseColorTexture");
|
||||||
|
WriteArtifactString(output, encodedTextureRef);
|
||||||
|
WriteArtifactString(output, "");
|
||||||
|
}
|
||||||
|
|
||||||
MaterialLoader loader;
|
MaterialLoader loader;
|
||||||
LoadResult result = loader.Load("Library/Manual/test.xcmat");
|
LoadResult result = loader.Load("Library/Manual/test.xcmat");
|
||||||
@@ -572,9 +772,8 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) {
|
|||||||
auto* material = static_cast<Material*>(result.resource);
|
auto* material = static_cast<Material*>(result.resource);
|
||||||
ASSERT_NE(material, nullptr);
|
ASSERT_NE(material, nullptr);
|
||||||
EXPECT_EQ(material->GetTextureBindingCount(), 1u);
|
EXPECT_EQ(material->GetTextureBindingCount(), 1u);
|
||||||
EXPECT_EQ(
|
EXPECT_TRUE(material->GetTextureBindingAssetRef(0).IsValid());
|
||||||
fs::path(material->GetTextureBindingPath(0).CStr()).lexically_normal().generic_string(),
|
EXPECT_TRUE(material->GetTextureBindingPath(0).Empty());
|
||||||
(projectRoot / "Assets" / "checker.bmp").lexically_normal().generic_string());
|
|
||||||
|
|
||||||
const ResourceHandle<Texture> initialTexture = material->GetTexture("baseColorTexture");
|
const ResourceHandle<Texture> initialTexture = material->GetTexture("baseColorTexture");
|
||||||
EXPECT_FALSE(initialTexture.IsValid());
|
EXPECT_FALSE(initialTexture.IsValid());
|
||||||
@@ -585,6 +784,9 @@ TEST(MaterialLoader, LoadMaterialArtifactDefersTexturePayloadUntilRequested) {
|
|||||||
ASSERT_TRUE(loadedTexture.IsValid());
|
ASSERT_TRUE(loadedTexture.IsValid());
|
||||||
EXPECT_EQ(loadedTexture->GetWidth(), 2u);
|
EXPECT_EQ(loadedTexture->GetWidth(), 2u);
|
||||||
EXPECT_EQ(loadedTexture->GetHeight(), 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;
|
delete material;
|
||||||
manager.SetResourceRoot("");
|
manager.SetResourceRoot("");
|
||||||
|
|||||||
Reference in New Issue
Block a user