From dd08d8969ee3c5e7ddb02bb81e38c81daba49367 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Thu, 2 Apr 2026 17:13:53 +0800 Subject: [PATCH] refactor: add minimal material gpu binding --- .../Pipelines/BuiltinForwardPipeline.h | 17 +- .../Rendering/RenderMaterialUtility.h | 70 +++++++ .../XCEngine/Resources/Material/Material.h | 3 + .../Pipelines/BuiltinForwardPipeline.cpp | 170 +++++++++++------ engine/src/Resources/Material/Material.cpp | 177 ++++++++++++++++-- .../unit/test_render_scene_extractor.cpp | 30 +++ tests/Resources/Material/test_material.cpp | 69 +++++++ 7 files changed, 455 insertions(+), 81 deletions(-) diff --git a/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h b/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h index 29bdba2d..4ebe55ec 100644 --- a/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h +++ b/engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h @@ -65,6 +65,17 @@ private: Math::Vector4 mainLightColorAndFlags = Math::Vector4::Zero(); }; + struct PerMaterialConstants { + Math::Vector4 baseColorFactor = Math::Vector4::One(); + }; + + struct CachedMaterialBindings { + OwnedDescriptorSet constantSet = {}; + OwnedDescriptorSet textureSet = {}; + Core::uint64 materialVersion = 0; + RHI::RHIResourceView* textureView = nullptr; + }; + bool EnsureInitialized(const RenderContext& context); bool CreatePipelineResources(const RenderContext& context); void DestroyPipelineResources(); @@ -72,7 +83,9 @@ private: const RenderContext& context, const Resources::Material* material); RHI::RHIDescriptorSet* GetOrCreatePerObjectSet(Core::uint64 objectId); - RHI::RHIDescriptorSet* GetOrCreateTextureSet(RHI::RHIResourceView* textureView); + CachedMaterialBindings* GetOrCreateMaterialBindings( + const Resources::Material* material, + RHI::RHIResourceView* textureView); void DestroyOwnedDescriptorSet(OwnedDescriptorSet& descriptorSet); const Resources::Texture* ResolveTexture(const Resources::Material* material) const; @@ -94,7 +107,7 @@ private: RHI::RHIPipelineLayout* m_pipelineLayout = nullptr; std::unordered_map m_pipelineStates; std::unordered_map m_perObjectSets; - std::unordered_map m_textureSets; + std::unordered_map m_materialBindings; RHI::RHISampler* m_sampler = nullptr; RHI::RHITexture* m_fallbackTexture = nullptr; RHI::RHIResourceView* m_fallbackTextureView = nullptr; diff --git a/engine/include/XCEngine/Rendering/RenderMaterialUtility.h b/engine/include/XCEngine/Rendering/RenderMaterialUtility.h index 4b32efd7..7cc2f249 100644 --- a/engine/include/XCEngine/Rendering/RenderMaterialUtility.h +++ b/engine/include/XCEngine/Rendering/RenderMaterialUtility.h @@ -118,6 +118,76 @@ inline bool ShaderPassMatchesBuiltinPass( return hasMetadata; } +struct BuiltinForwardMaterialData { + Math::Vector4 baseColorFactor = Math::Vector4::One(); +}; + +inline Math::Vector4 ResolveBuiltinBaseColorFactor(const Resources::Material* material) { + if (material == nullptr) { + return Math::Vector4::One(); + } + + static const char* kBaseColorPropertyNames[] = { + "baseColor", + "_BaseColor", + "color", + "_Color" + }; + + for (const char* propertyName : kBaseColorPropertyNames) { + if (material->HasProperty(Containers::String(propertyName))) { + return material->GetFloat4(Containers::String(propertyName)); + } + } + + Math::Vector4 baseColor = Math::Vector4::One(); + static const char* kOpacityPropertyNames[] = { + "opacity", + "_Opacity", + "alpha", + "_Alpha" + }; + for (const char* propertyName : kOpacityPropertyNames) { + if (material->HasProperty(Containers::String(propertyName))) { + baseColor.w = material->GetFloat(Containers::String(propertyName)); + break; + } + } + + return baseColor; +} + +inline const Resources::Texture* ResolveBuiltinBaseColorTexture(const Resources::Material* material) { + if (material == nullptr) { + return nullptr; + } + + static const char* kTextureNames[] = { + "baseColorTexture", + "_BaseColorTexture", + "_MainTex", + "albedoTexture", + "mainTexture", + "texture" + }; + + for (const char* textureName : kTextureNames) { + const Resources::ResourceHandle textureHandle = + material->GetTexture(Containers::String(textureName)); + if (textureHandle.Get() != nullptr && textureHandle->IsValid()) { + return textureHandle.Get(); + } + } + + return nullptr; +} + +inline BuiltinForwardMaterialData BuildBuiltinForwardMaterialData(const Resources::Material* material) { + BuiltinForwardMaterialData data = {}; + data.baseColorFactor = ResolveBuiltinBaseColorFactor(material); + return data; +} + inline const Resources::Material* ResolveMaterial( const Components::MeshRendererComponent* meshRenderer, const Resources::Mesh* mesh, diff --git a/engine/include/XCEngine/Resources/Material/Material.h b/engine/include/XCEngine/Resources/Material/Material.h index 42187fc0..526cb4d0 100644 --- a/engine/include/XCEngine/Resources/Material/Material.h +++ b/engine/include/XCEngine/Resources/Material/Material.h @@ -197,6 +197,7 @@ public: const Containers::Array& GetConstantBufferData() const { return m_constantBufferData; } void UpdateConstantBuffer(); + Core::uint64 GetChangeVersion() const { return m_changeVersion; } void RecalculateMemorySize(); bool HasProperty(const Containers::String& name) const; @@ -204,6 +205,7 @@ public: void ClearAllProperties(); private: + void MarkChanged(bool updateConstantBuffer); void UpdateMemorySize(); ResourceHandle m_shader; @@ -214,6 +216,7 @@ private: Containers::HashMap m_properties; Containers::Array m_constantBufferData; Containers::Array m_textureBindings; + Core::uint64 m_changeVersion = 1; }; } // namespace Resources diff --git a/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp b/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp index 719a3c80..1b20a2b2 100644 --- a/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp +++ b/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp @@ -49,7 +49,7 @@ private: namespace { constexpr uint32_t kDescriptorFirstSet = 1; -constexpr uint32_t kDescriptorSetCount = 4; +constexpr uint32_t kDescriptorSetCount = 5; const char kBuiltinForwardHlsl[] = R"( Texture2D gBaseColorTexture : register(t1); @@ -64,6 +64,10 @@ cbuffer PerObjectConstants : register(b1) { float4 gMainLightColorAndFlags; }; +cbuffer MaterialConstants : register(b2) { + float4 gBaseColorFactor; +}; + struct VSInput { float3 position : POSITION; float3 normal : NORMAL; @@ -87,7 +91,7 @@ PSInput MainVS(VSInput input) { } float4 MainPS(PSInput input) : SV_TARGET { - float4 baseColor = gBaseColorTexture.Sample(gLinearSampler, input.texcoord); + float4 baseColor = gBaseColorTexture.Sample(gLinearSampler, input.texcoord) * gBaseColorFactor; if (gMainLightColorAndFlags.a < 0.5f) { return baseColor; } @@ -139,13 +143,17 @@ layout(std140, binding = 1) uniform PerObjectConstants { vec4 gMainLightColorAndFlags; }; +layout(std140, binding = 2) uniform MaterialConstants { + vec4 gBaseColorFactor; +}; + in vec3 vNormalWS; in vec2 vTexCoord; layout(location = 0) out vec4 fragColor; void main() { - vec4 baseColor = texture(uBaseColorTexture, vTexCoord); + vec4 baseColor = texture(uBaseColorTexture, vTexCoord) * gBaseColorFactor; if (gMainLightColorAndFlags.w < 0.5) { fragColor = baseColor; return; @@ -187,8 +195,8 @@ void main() { )"; const char kBuiltinForwardVulkanFragmentShader[] = R"(#version 450 -layout(set = 2, binding = 0) uniform texture2D uBaseColorTexture; -layout(set = 3, binding = 0) uniform sampler uLinearSampler; +layout(set = 3, binding = 0) uniform texture2D uBaseColorTexture; +layout(set = 4, binding = 0) uniform sampler uLinearSampler; layout(set = 1, binding = 0, std140) uniform PerObjectConstants { mat4 gProjectionMatrix; @@ -199,13 +207,17 @@ layout(set = 1, binding = 0, std140) uniform PerObjectConstants { vec4 gMainLightColorAndFlags; }; +layout(set = 2, binding = 0, std140) uniform MaterialConstants { + vec4 gBaseColorFactor; +}; + layout(location = 0) in vec3 vNormalWS; layout(location = 1) in vec2 vTexCoord; layout(location = 0) out vec4 fragColor; void main() { - vec4 baseColor = texture(sampler2D(uBaseColorTexture, uLinearSampler), vTexCoord); + vec4 baseColor = texture(sampler2D(uBaseColorTexture, uLinearSampler), vTexCoord) * gBaseColorFactor; if (gMainLightColorAndFlags.w < 0.5) { fragColor = baseColor; return; @@ -277,26 +289,6 @@ RHI::GraphicsPipelineDesc CreatePipelineDesc( return pipelineDesc; } -const Resources::Texture* FindMaterialTexture(const Resources::Material& material) { - static const char* kTextureNames[] = { - "baseColorTexture", - "_BaseColorTexture", - "_MainTex", - "albedoTexture", - "mainTexture", - "texture" - }; - - for (const char* textureName : kTextureNames) { - const Resources::ResourceHandle textureHandle = material.GetTexture(textureName); - if (textureHandle.Get() != nullptr && textureHandle->IsValid()) { - return textureHandle.Get(); - } - } - - return nullptr; -} - } // namespace BuiltinForwardPipeline::BuiltinForwardPipeline() { @@ -497,6 +489,10 @@ bool BuiltinForwardPipeline::CreatePipelineResources(const RenderContext& contex constantLayout.bindings = &constantBinding; constantLayout.bindingCount = 1; + RHI::DescriptorSetLayoutDesc materialConstantLayout = {}; + materialConstantLayout.bindings = &constantBinding; + materialConstantLayout.bindingCount = 1; + RHI::DescriptorSetLayoutBinding textureBinding = {}; textureBinding.binding = 0; textureBinding.type = static_cast(RHI::DescriptorType::SRV); @@ -546,8 +542,9 @@ bool BuiltinForwardPipeline::CreatePipelineResources(const RenderContext& contex RHI::DescriptorSetLayoutDesc setLayouts[kDescriptorSetCount] = {}; setLayouts[0] = reservedLayout; setLayouts[1] = constantLayout; - setLayouts[2] = textureLayout; - setLayouts[3] = samplerLayout; + setLayouts[2] = materialConstantLayout; + setLayouts[3] = textureLayout; + setLayouts[4] = samplerLayout; RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {}; pipelineLayoutDesc.setLayouts = setLayouts; @@ -618,10 +615,11 @@ void BuiltinForwardPipeline::DestroyPipelineResources() { } m_perObjectSets.clear(); - for (auto& textureSetPair : m_textureSets) { - DestroyOwnedDescriptorSet(textureSetPair.second); + for (auto& materialBindingPair : m_materialBindings) { + DestroyOwnedDescriptorSet(materialBindingPair.second.constantSet); + DestroyOwnedDescriptorSet(materialBindingPair.second.textureSet); } - m_textureSets.clear(); + m_materialBindings.clear(); if (m_fallbackTextureView != nullptr) { m_fallbackTextureView->Shutdown(); @@ -727,44 +725,82 @@ RHI::RHIDescriptorSet* BuiltinForwardPipeline::GetOrCreatePerObjectSet(Core::uin return result.first->second.set; } -RHI::RHIDescriptorSet* BuiltinForwardPipeline::GetOrCreateTextureSet(RHI::RHIResourceView* textureView) { +BuiltinForwardPipeline::CachedMaterialBindings* BuiltinForwardPipeline::GetOrCreateMaterialBindings( + const Resources::Material* material, + RHI::RHIResourceView* textureView) { if (textureView == nullptr) { return nullptr; } - const auto existing = m_textureSets.find(textureView); - if (existing != m_textureSets.end()) { - return existing->second.set; + CachedMaterialBindings& cachedBindings = m_materialBindings[material]; + + if (cachedBindings.constantSet.set == nullptr) { + RHI::DescriptorPoolDesc constantPoolDesc = {}; + constantPoolDesc.type = RHI::DescriptorHeapType::CBV_SRV_UAV; + constantPoolDesc.descriptorCount = 1; + constantPoolDesc.shaderVisible = false; + + cachedBindings.constantSet.pool = m_device->CreateDescriptorPool(constantPoolDesc); + if (cachedBindings.constantSet.pool == nullptr) { + return nullptr; + } + + RHI::DescriptorSetLayoutBinding constantBinding = {}; + constantBinding.binding = 0; + constantBinding.type = static_cast(RHI::DescriptorType::CBV); + constantBinding.count = 1; + + RHI::DescriptorSetLayoutDesc constantLayout = {}; + constantLayout.bindings = &constantBinding; + constantLayout.bindingCount = 1; + cachedBindings.constantSet.set = cachedBindings.constantSet.pool->AllocateSet(constantLayout); + if (cachedBindings.constantSet.set == nullptr) { + DestroyOwnedDescriptorSet(cachedBindings.constantSet); + return nullptr; + } } - RHI::DescriptorPoolDesc poolDesc = {}; - poolDesc.type = RHI::DescriptorHeapType::CBV_SRV_UAV; - poolDesc.descriptorCount = 1; - poolDesc.shaderVisible = true; + if (cachedBindings.textureSet.set == nullptr) { + RHI::DescriptorPoolDesc texturePoolDesc = {}; + texturePoolDesc.type = RHI::DescriptorHeapType::CBV_SRV_UAV; + texturePoolDesc.descriptorCount = 1; + texturePoolDesc.shaderVisible = true; - OwnedDescriptorSet descriptorSet = {}; - descriptorSet.pool = m_device->CreateDescriptorPool(poolDesc); - if (descriptorSet.pool == nullptr) { - return nullptr; + cachedBindings.textureSet.pool = m_device->CreateDescriptorPool(texturePoolDesc); + if (cachedBindings.textureSet.pool == nullptr) { + DestroyOwnedDescriptorSet(cachedBindings.constantSet); + return nullptr; + } + + RHI::DescriptorSetLayoutBinding textureBinding = {}; + textureBinding.binding = 0; + textureBinding.type = static_cast(RHI::DescriptorType::SRV); + textureBinding.count = 1; + + RHI::DescriptorSetLayoutDesc textureLayout = {}; + textureLayout.bindings = &textureBinding; + textureLayout.bindingCount = 1; + cachedBindings.textureSet.set = cachedBindings.textureSet.pool->AllocateSet(textureLayout); + if (cachedBindings.textureSet.set == nullptr) { + DestroyOwnedDescriptorSet(cachedBindings.textureSet); + return nullptr; + } } - RHI::DescriptorSetLayoutBinding binding = {}; - binding.binding = 0; - binding.type = static_cast(RHI::DescriptorType::SRV); - binding.count = 1; + const Core::uint64 materialVersion = material != nullptr ? material->GetChangeVersion() : 0; + if (cachedBindings.materialVersion != materialVersion || cachedBindings.textureView != textureView) { + const BuiltinForwardMaterialData materialData = BuildBuiltinForwardMaterialData(material); + const PerMaterialConstants materialConstants = { + materialData.baseColorFactor + }; - RHI::DescriptorSetLayoutDesc layout = {}; - layout.bindings = &binding; - layout.bindingCount = 1; - descriptorSet.set = descriptorSet.pool->AllocateSet(layout); - if (descriptorSet.set == nullptr) { - DestroyOwnedDescriptorSet(descriptorSet); - return nullptr; + cachedBindings.constantSet.set->WriteConstant(0, &materialConstants, sizeof(materialConstants)); + cachedBindings.textureSet.set->Update(0, textureView); + cachedBindings.materialVersion = materialVersion; + cachedBindings.textureView = textureView; } - descriptorSet.set->Update(0, textureView); - const auto result = m_textureSets.emplace(textureView, descriptorSet); - return result.first->second.set; + return &cachedBindings; } void BuiltinForwardPipeline::DestroyOwnedDescriptorSet(OwnedDescriptorSet& descriptorSet) { @@ -782,7 +818,7 @@ void BuiltinForwardPipeline::DestroyOwnedDescriptorSet(OwnedDescriptorSet& descr } const Resources::Texture* BuiltinForwardPipeline::ResolveTexture(const Resources::Material* material) const { - return material != nullptr ? FindMaterialTexture(*material) : nullptr; + return ResolveBuiltinBaseColorTexture(material); } RHI::RHIResourceView* BuiltinForwardPipeline::ResolveTextureView( @@ -844,16 +880,26 @@ bool BuiltinForwardPipeline::DrawVisibleItem( return false; } + const Resources::Material* material = ResolveMaterial(visibleItem); RHI::RHIDescriptorSet* constantSet = GetOrCreatePerObjectSet( visibleItem.gameObject != nullptr ? visibleItem.gameObject->GetID() : 0); - RHI::RHIDescriptorSet* textureSet = GetOrCreateTextureSet(textureView); - if (constantSet == nullptr || textureSet == nullptr || m_samplerSet == nullptr) { + CachedMaterialBindings* materialBindings = GetOrCreateMaterialBindings(material, textureView); + if (constantSet == nullptr || + materialBindings == nullptr || + materialBindings->constantSet.set == nullptr || + materialBindings->textureSet.set == nullptr || + m_samplerSet == nullptr) { return false; } constantSet->WriteConstant(0, &constants, sizeof(constants)); - RHI::RHIDescriptorSet* descriptorSets[] = { constantSet, textureSet, m_samplerSet }; - commandList->SetGraphicsDescriptorSets(kDescriptorFirstSet, 3, descriptorSets, m_pipelineLayout); + RHI::RHIDescriptorSet* descriptorSets[] = { + constantSet, + materialBindings->constantSet.set, + materialBindings->textureSet.set, + m_samplerSet + }; + commandList->SetGraphicsDescriptorSets(kDescriptorFirstSet, 4, descriptorSets, m_pipelineLayout); if (visibleItem.hasSection) { const Containers::Array& sections = visibleItem.mesh->GetSections(); diff --git a/engine/src/Resources/Material/Material.cpp b/engine/src/Resources/Material/Material.cpp index 022b96ff..8eb82d56 100644 --- a/engine/src/Resources/Material/Material.cpp +++ b/engine/src/Resources/Material/Material.cpp @@ -2,9 +2,98 @@ #include #include +#include +#include +#include +#include + namespace XCEngine { namespace Resources { +namespace { + +constexpr size_t kMaterialConstantSlotSize = 16; + +bool IsPackedMaterialPropertyType(MaterialPropertyType type) { + switch (type) { + case MaterialPropertyType::Float: + case MaterialPropertyType::Float2: + case MaterialPropertyType::Float3: + case MaterialPropertyType::Float4: + case MaterialPropertyType::Int: + case MaterialPropertyType::Int2: + case MaterialPropertyType::Int3: + case MaterialPropertyType::Int4: + case MaterialPropertyType::Bool: + return true; + case MaterialPropertyType::Texture: + case MaterialPropertyType::Cubemap: + default: + return false; + } +} + +void RemoveTextureBindingByName( + Containers::Array& textureBindings, + const Containers::String& name) { + for (size_t bindingIndex = 0; bindingIndex < textureBindings.Size(); ++bindingIndex) { + if (textureBindings[bindingIndex].name == name) { + if (bindingIndex != textureBindings.Size() - 1) { + textureBindings[bindingIndex] = std::move(textureBindings.Back()); + textureBindings[bindingIndex].slot = static_cast(bindingIndex); + } + textureBindings.PopBack(); + break; + } + } + + for (size_t bindingIndex = 0; bindingIndex < textureBindings.Size(); ++bindingIndex) { + textureBindings[bindingIndex].slot = static_cast(bindingIndex); + } +} + +void WritePackedMaterialProperty(Core::uint8* destination, const MaterialProperty& property) { + std::memset(destination, 0, kMaterialConstantSlotSize); + + switch (property.type) { + case MaterialPropertyType::Float: + std::memcpy(destination, property.value.floatValue, sizeof(float)); + break; + case MaterialPropertyType::Float2: + std::memcpy(destination, property.value.floatValue, sizeof(float) * 2); + break; + case MaterialPropertyType::Float3: + std::memcpy(destination, property.value.floatValue, sizeof(float) * 3); + break; + case MaterialPropertyType::Float4: + std::memcpy(destination, property.value.floatValue, sizeof(float) * 4); + break; + case MaterialPropertyType::Int: + std::memcpy(destination, property.value.intValue, sizeof(Core::int32)); + break; + case MaterialPropertyType::Int2: + std::memcpy(destination, property.value.intValue, sizeof(Core::int32) * 2); + break; + case MaterialPropertyType::Int3: + std::memcpy(destination, property.value.intValue, sizeof(Core::int32) * 3); + break; + case MaterialPropertyType::Int4: + std::memcpy(destination, property.value.intValue, sizeof(Core::int32) * 4); + break; + case MaterialPropertyType::Bool: { + const Core::uint32 boolValue = property.value.boolValue ? 1u : 0u; + std::memcpy(destination, &boolValue, sizeof(boolValue)); + break; + } + case MaterialPropertyType::Texture: + case MaterialPropertyType::Cubemap: + default: + break; + } +} + +} // namespace + Material::Material() = default; Material::~Material() = default; @@ -18,17 +107,19 @@ void Material::Release() { m_properties.Clear(); m_textureBindings.Clear(); m_constantBufferData.Clear(); + m_changeVersion = 1; m_isValid = false; UpdateMemorySize(); } void Material::SetShader(const ResourceHandle& shader) { m_shader = shader; - UpdateMemorySize(); + MarkChanged(false); } void Material::SetRenderQueue(Core::int32 renderQueue) { m_renderQueue = renderQueue; + MarkChanged(false); } void Material::SetRenderQueue(MaterialRenderQueue renderQueue) { @@ -37,19 +128,19 @@ void Material::SetRenderQueue(MaterialRenderQueue renderQueue) { void Material::SetRenderState(const MaterialRenderState& renderState) { m_renderState = renderState; - UpdateMemorySize(); + MarkChanged(false); } void Material::SetShaderPass(const Containers::String& shaderPass) { m_shaderPass = shaderPass; - UpdateMemorySize(); + MarkChanged(false); } void Material::SetTag(const Containers::String& name, const Containers::String& value) { for (MaterialTagEntry& tag : m_tags) { if (tag.name == name) { tag.value = value; - UpdateMemorySize(); + MarkChanged(false); return; } } @@ -58,7 +149,7 @@ void Material::SetTag(const Containers::String& name, const Containers::String& tag.name = name; tag.value = value; m_tags.PushBack(tag); - UpdateMemorySize(); + MarkChanged(false); } Containers::String Material::GetTag(const Containers::String& name) const { @@ -88,7 +179,7 @@ void Material::RemoveTag(const Containers::String& name) { m_tags[tagIndex] = std::move(m_tags.Back()); } m_tags.PopBack(); - UpdateMemorySize(); + MarkChanged(false); return; } } @@ -96,7 +187,7 @@ void Material::RemoveTag(const Containers::String& name) { void Material::ClearTags() { m_tags.Clear(); - UpdateMemorySize(); + MarkChanged(false); } Containers::String Material::GetTagName(Core::uint32 index) const { @@ -108,16 +199,18 @@ Containers::String Material::GetTagValue(Core::uint32 index) const { } void Material::SetFloat(const Containers::String& name, float value) { + RemoveTextureBindingByName(m_textureBindings, name); MaterialProperty prop; prop.name = name; prop.type = MaterialPropertyType::Float; prop.value.floatValue[0] = value; prop.refCount = 1; m_properties.Insert(name, prop); - UpdateMemorySize(); + MarkChanged(true); } void Material::SetFloat2(const Containers::String& name, const Math::Vector2& value) { + RemoveTextureBindingByName(m_textureBindings, name); MaterialProperty prop; prop.name = name; prop.type = MaterialPropertyType::Float2; @@ -125,10 +218,11 @@ void Material::SetFloat2(const Containers::String& name, const Math::Vector2& va prop.value.floatValue[1] = value.y; prop.refCount = 1; m_properties.Insert(name, prop); - UpdateMemorySize(); + MarkChanged(true); } void Material::SetFloat3(const Containers::String& name, const Math::Vector3& value) { + RemoveTextureBindingByName(m_textureBindings, name); MaterialProperty prop; prop.name = name; prop.type = MaterialPropertyType::Float3; @@ -137,10 +231,11 @@ void Material::SetFloat3(const Containers::String& name, const Math::Vector3& va prop.value.floatValue[2] = value.z; prop.refCount = 1; m_properties.Insert(name, prop); - UpdateMemorySize(); + MarkChanged(true); } void Material::SetFloat4(const Containers::String& name, const Math::Vector4& value) { + RemoveTextureBindingByName(m_textureBindings, name); MaterialProperty prop; prop.name = name; prop.type = MaterialPropertyType::Float4; @@ -150,27 +245,29 @@ void Material::SetFloat4(const Containers::String& name, const Math::Vector4& va prop.value.floatValue[3] = value.w; prop.refCount = 1; m_properties.Insert(name, prop); - UpdateMemorySize(); + MarkChanged(true); } void Material::SetInt(const Containers::String& name, Core::int32 value) { + RemoveTextureBindingByName(m_textureBindings, name); MaterialProperty prop; prop.name = name; prop.type = MaterialPropertyType::Int; prop.value.intValue[0] = value; prop.refCount = 1; m_properties.Insert(name, prop); - UpdateMemorySize(); + MarkChanged(true); } void Material::SetBool(const Containers::String& name, bool value) { + RemoveTextureBindingByName(m_textureBindings, name); MaterialProperty prop; prop.name = name; prop.type = MaterialPropertyType::Bool; prop.value.boolValue = value; prop.refCount = 1; m_properties.Insert(name, prop); - UpdateMemorySize(); + MarkChanged(true); } void Material::SetTexture(const Containers::String& name, const ResourceHandle& texture) { @@ -183,7 +280,7 @@ void Material::SetTexture(const Containers::String& name, const ResourceHandle(m_textureBindings.Size()); binding.texture = texture; m_textureBindings.PushBack(binding); - UpdateMemorySize(); + MarkChanged(false); } float Material::GetFloat(const Containers::String& name) const { @@ -273,7 +370,33 @@ std::vector Material::GetProperties() const { } void Material::UpdateConstantBuffer() { + std::vector packedProperties; + const auto pairs = m_properties.GetPairs(); + packedProperties.reserve(pairs.Size()); + for (const auto& pair : pairs) { + if (IsPackedMaterialPropertyType(pair.second.type)) { + packedProperties.push_back(&pair.second); + } + } + + std::sort( + packedProperties.begin(), + packedProperties.end(), + [](const MaterialProperty* left, const MaterialProperty* right) { + return std::strcmp(left->name.CStr(), right->name.CStr()) < 0; + }); + m_constantBufferData.Clear(); + m_constantBufferData.Resize(packedProperties.size() * kMaterialConstantSlotSize); + if (!packedProperties.empty()) { + std::memset(m_constantBufferData.Data(), 0, m_constantBufferData.Size()); + } + + for (size_t propertyIndex = 0; propertyIndex < packedProperties.size(); ++propertyIndex) { + WritePackedMaterialProperty( + m_constantBufferData.Data() + propertyIndex * kMaterialConstantSlotSize, + *packedProperties[propertyIndex]); + } UpdateMemorySize(); } @@ -286,15 +409,35 @@ bool Material::HasProperty(const Containers::String& name) const { } void Material::RemoveProperty(const Containers::String& name) { + const MaterialProperty* property = m_properties.Find(name); + const bool removeTextureBinding = + property != nullptr && + (property->type == MaterialPropertyType::Texture || property->type == MaterialPropertyType::Cubemap); + m_properties.Erase(name); - UpdateMemorySize(); + + if (removeTextureBinding) { + RemoveTextureBindingByName(m_textureBindings, name); + } + + MarkChanged(true); } void Material::ClearAllProperties() { m_properties.Clear(); m_textureBindings.Clear(); m_constantBufferData.Clear(); - UpdateMemorySize(); + MarkChanged(false); +} + +void Material::MarkChanged(bool updateConstantBuffer) { + if (updateConstantBuffer) { + UpdateConstantBuffer(); + } else { + UpdateMemorySize(); + } + + ++m_changeVersion; } void Material::UpdateMemorySize() { diff --git a/tests/Rendering/unit/test_render_scene_extractor.cpp b/tests/Rendering/unit/test_render_scene_extractor.cpp index 8883b9c8..9dd32859 100644 --- a/tests/Rendering/unit/test_render_scene_extractor.cpp +++ b/tests/Rendering/unit/test_render_scene_extractor.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include using namespace XCEngine::Components; @@ -425,6 +426,35 @@ TEST(RenderMaterialUtility_Test, ExplicitShaderPassMetadataDisablesImplicitForwa EXPECT_TRUE(MatchesBuiltinPass(&material, BuiltinMaterialPass::Unlit)); } +TEST(RenderMaterialUtility_Test, ResolvesBuiltinForwardMaterialContractFromCanonicalNamesAndAliases) { + Material canonicalMaterial; + canonicalMaterial.SetFloat4("baseColor", Vector4(0.2f, 0.4f, 0.6f, 0.8f)); + EXPECT_EQ(ResolveBuiltinBaseColorFactor(&canonicalMaterial), Vector4(0.2f, 0.4f, 0.6f, 0.8f)); + + Material aliasMaterial; + aliasMaterial.SetFloat4("_BaseColor", Vector4(0.7f, 0.6f, 0.5f, 0.4f)); + Texture* baseColorTexture = new Texture(); + IResource::ConstructParams textureParams = {}; + textureParams.name = "AliasBaseColor"; + textureParams.path = "Textures/alias_base_color.texture"; + textureParams.guid = ResourceGUID::Generate(textureParams.path); + baseColorTexture->Initialize(textureParams); + aliasMaterial.SetTexture("_BaseColorTexture", ResourceHandle(baseColorTexture)); + + const BuiltinForwardMaterialData materialData = BuildBuiltinForwardMaterialData(&aliasMaterial); + EXPECT_EQ(materialData.baseColorFactor, Vector4(0.7f, 0.6f, 0.5f, 0.4f)); + EXPECT_EQ(ResolveBuiltinBaseColorTexture(&aliasMaterial), baseColorTexture); +} + +TEST(RenderMaterialUtility_Test, UsesOpacityOnlyWhenBaseColorFactorIsMissing) { + Material material; + material.SetFloat("opacity", 0.35f); + EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(1.0f, 1.0f, 1.0f, 0.35f)); + + material.SetFloat4("baseColor", Vector4(0.9f, 0.8f, 0.7f, 0.6f)); + EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(0.9f, 0.8f, 0.7f, 0.6f)); +} + TEST(RenderMaterialUtility_Test, MapsMaterialRenderStateToRhiDescriptors) { Material material; MaterialRenderState renderState; diff --git a/tests/Resources/Material/test_material.cpp b/tests/Resources/Material/test_material.cpp index cc75f869..c42689e6 100644 --- a/tests/Resources/Material/test_material.cpp +++ b/tests/Resources/Material/test_material.cpp @@ -225,6 +225,75 @@ TEST(Material, SetTextureReplacesExistingBinding) { EXPECT_EQ(material.GetTexture("uDiffuse").Get(), secondTexture); } +TEST(Material, ChangeVersionIncrementsWhenMaterialMutates) { + Material material; + const XCEngine::Core::uint64 initialVersion = material.GetChangeVersion(); + + material.SetFloat("uTime", 1.0f); + const XCEngine::Core::uint64 afterFloatVersion = material.GetChangeVersion(); + EXPECT_GT(afterFloatVersion, initialVersion); + + material.SetTexture("uDiffuse", ResourceHandle(new Texture())); + EXPECT_GT(material.GetChangeVersion(), afterFloatVersion); +} + +TEST(Material, UpdateConstantBufferPacksNumericPropertiesIntoStableSlots) { + Material material; + material.SetFloat("alpha", 3.5f); + material.SetFloat4("beta", Vector4(1.0f, 2.0f, 3.0f, 4.0f)); + material.SetInt("gamma", 7); + + const auto& constantBufferData = material.GetConstantBufferData(); + ASSERT_EQ(constantBufferData.Size(), 48u); + + const float* alphaSlot = reinterpret_cast(constantBufferData.Data()); + EXPECT_FLOAT_EQ(alphaSlot[0], 3.5f); + EXPECT_FLOAT_EQ(alphaSlot[1], 0.0f); + EXPECT_FLOAT_EQ(alphaSlot[2], 0.0f); + EXPECT_FLOAT_EQ(alphaSlot[3], 0.0f); + + const float* betaSlot = reinterpret_cast(constantBufferData.Data() + 16); + EXPECT_FLOAT_EQ(betaSlot[0], 1.0f); + EXPECT_FLOAT_EQ(betaSlot[1], 2.0f); + EXPECT_FLOAT_EQ(betaSlot[2], 3.0f); + EXPECT_FLOAT_EQ(betaSlot[3], 4.0f); + + const XCEngine::Core::int32* gammaSlot = + reinterpret_cast(constantBufferData.Data() + 32); + EXPECT_EQ(gammaSlot[0], 7); + EXPECT_EQ(gammaSlot[1], 0); + EXPECT_EQ(gammaSlot[2], 0); + EXPECT_EQ(gammaSlot[3], 0); +} + +TEST(Material, RemoveTexturePropertyAlsoRemovesTextureBinding) { + Material material; + Texture* texture = new Texture(); + + material.SetTexture("uDiffuse", ResourceHandle(texture)); + ASSERT_EQ(material.GetTextureBindingCount(), 1u); + + material.RemoveProperty("uDiffuse"); + + EXPECT_FALSE(material.HasProperty("uDiffuse")); + EXPECT_EQ(material.GetTextureBindingCount(), 0u); + EXPECT_EQ(material.GetTexture("uDiffuse").Get(), nullptr); +} + +TEST(Material, ReplacingTexturePropertyWithScalarRemovesTextureBinding) { + Material material; + Texture* texture = new Texture(); + + material.SetTexture("uMain", ResourceHandle(texture)); + ASSERT_EQ(material.GetTextureBindingCount(), 1u); + + material.SetFloat("uMain", 2.0f); + + EXPECT_EQ(material.GetTextureBindingCount(), 0u); + EXPECT_EQ(material.GetTexture("uMain").Get(), nullptr); + EXPECT_FLOAT_EQ(material.GetFloat("uMain"), 2.0f); +} + TEST(Material, HasProperty) { Material material;