diff --git a/engine/include/XCEngine/Resources/Material/Material.h b/engine/include/XCEngine/Resources/Material/Material.h index bbff7503..5f76d7d8 100644 --- a/engine/include/XCEngine/Resources/Material/Material.h +++ b/engine/include/XCEngine/Resources/Material/Material.h @@ -68,6 +68,7 @@ public: Core::int32 GetInt(const Containers::String& name) const; bool GetBool(const Containers::String& name) const; ResourceHandle GetTexture(const Containers::String& name) const; + Core::uint32 GetTextureBindingCount() const { return static_cast(m_textureBindings.Size()); } const Containers::Array& GetConstantBufferData() const { return m_constantBufferData; } void UpdateConstantBuffer(); @@ -77,6 +78,8 @@ public: void ClearAllProperties(); private: + void UpdateMemorySize(); + ResourceHandle m_shader; Containers::HashMap m_properties; Containers::Array m_constantBufferData; diff --git a/engine/include/XCEngine/Resources/Mesh/Mesh.h b/engine/include/XCEngine/Resources/Mesh/Mesh.h index 854a5012..77bbd229 100644 --- a/engine/include/XCEngine/Resources/Mesh/Mesh.h +++ b/engine/include/XCEngine/Resources/Mesh/Mesh.h @@ -10,6 +10,9 @@ namespace XCEngine { namespace Resources { +class Material; +class Texture; + enum class VertexAttribute : Core::uint32 { Position = 1 << 0, Normal = 1 << 1, Tangent = 1 << 2, Color = 1 << 3, UV0 = 1 << 4, UV1 = 1 << 5, @@ -77,7 +80,12 @@ public: const Math::Bounds& GetBounds() const { return m_bounds; } void AddSection(const MeshSection& section); + void AddMaterial(Material* material); + void AddTexture(Texture* texture); const Containers::Array& GetSections() const { return m_sections; } + const Containers::Array& GetMaterials() const { return m_materials; } + const Containers::Array& GetTextures() const { return m_textures; } + Material* GetMaterial(Core::uint32 index) const; private: void UpdateMemorySize(); @@ -92,6 +100,8 @@ private: Containers::Array m_vertexData; Containers::Array m_indexData; Containers::Array m_sections; + Containers::Array m_materials; + Containers::Array m_textures; Math::Bounds m_bounds; }; diff --git a/engine/include/XCEngine/Resources/Mesh/MeshImportSettings.h b/engine/include/XCEngine/Resources/Mesh/MeshImportSettings.h index c432ee49..d091719f 100644 --- a/engine/include/XCEngine/Resources/Mesh/MeshImportSettings.h +++ b/engine/include/XCEngine/Resources/Mesh/MeshImportSettings.h @@ -71,7 +71,7 @@ public: float GetThreshold() const { return m_threshold; } private: - MeshImportFlags m_importFlags = MeshImportFlags::None; + MeshImportFlags m_importFlags = MeshImportFlags::ImportMaterials; float m_scale = 1.0f; Math::Vector3 m_offset = Math::Vector3::Zero(); bool m_axisConversion = true; diff --git a/engine/include/XCEngine/Resources/Texture/TextureLoader.h b/engine/include/XCEngine/Resources/Texture/TextureLoader.h index 05feaeb2..015ce0b8 100644 --- a/engine/include/XCEngine/Resources/Texture/TextureLoader.h +++ b/engine/include/XCEngine/Resources/Texture/TextureLoader.h @@ -15,6 +15,7 @@ public: Containers::Array GetSupportedExtensions() const override; bool CanLoad(const Containers::String& path) const override; LoadResult Load(const Containers::String& path, const ImportSettings* settings = nullptr) override; + LoadResult LoadFromMemory(const Containers::String& path, const void* data, size_t dataSize) const; ImportSettings* GetDefaultSettings() const override; }; diff --git a/engine/src/Resources/Material/Material.cpp b/engine/src/Resources/Material/Material.cpp index 87516267..a3e9f480 100644 --- a/engine/src/Resources/Material/Material.cpp +++ b/engine/src/Resources/Material/Material.cpp @@ -15,10 +15,12 @@ void Material::Release() { m_textureBindings.Clear(); m_constantBufferData.Clear(); m_isValid = false; + UpdateMemorySize(); } void Material::SetShader(const ResourceHandle& shader) { m_shader = shader; + UpdateMemorySize(); } void Material::SetFloat(const Containers::String& name, float value) { @@ -28,6 +30,7 @@ void Material::SetFloat(const Containers::String& name, float value) { prop.value.floatValue[0] = value; prop.refCount = 1; m_properties.Insert(name, prop); + UpdateMemorySize(); } void Material::SetFloat2(const Containers::String& name, const Math::Vector2& value) { @@ -38,6 +41,7 @@ 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(); } void Material::SetFloat3(const Containers::String& name, const Math::Vector3& value) { @@ -49,6 +53,7 @@ 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(); } void Material::SetFloat4(const Containers::String& name, const Math::Vector4& value) { @@ -61,6 +66,7 @@ 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(); } void Material::SetInt(const Containers::String& name, Core::int32 value) { @@ -70,6 +76,7 @@ void Material::SetInt(const Containers::String& name, Core::int32 value) { prop.value.intValue[0] = value; prop.refCount = 1; m_properties.Insert(name, prop); + UpdateMemorySize(); } void Material::SetBool(const Containers::String& name, bool value) { @@ -79,6 +86,7 @@ void Material::SetBool(const Containers::String& name, bool value) { prop.value.boolValue = value; prop.refCount = 1; m_properties.Insert(name, prop); + UpdateMemorySize(); } void Material::SetTexture(const Containers::String& name, const ResourceHandle& texture) { @@ -87,12 +95,21 @@ void Material::SetTexture(const Containers::String& name, const ResourceHandle(m_textureBindings.Size()); binding.texture = texture; m_textureBindings.PushBack(binding); + UpdateMemorySize(); } float Material::GetFloat(const Containers::String& name) const { @@ -155,6 +172,7 @@ ResourceHandle Material::GetTexture(const Containers::String& name) con void Material::UpdateConstantBuffer() { m_constantBufferData.Clear(); + UpdateMemorySize(); } bool Material::HasProperty(const Containers::String& name) const { @@ -163,12 +181,26 @@ bool Material::HasProperty(const Containers::String& name) const { void Material::RemoveProperty(const Containers::String& name) { m_properties.Erase(name); + UpdateMemorySize(); } void Material::ClearAllProperties() { m_properties.Clear(); m_textureBindings.Clear(); m_constantBufferData.Clear(); + UpdateMemorySize(); +} + +void Material::UpdateMemorySize() { + m_memorySize = m_constantBufferData.Size() + + m_textureBindings.Size() * sizeof(TextureBinding) + + m_properties.Size() * sizeof(MaterialProperty) + + m_name.Length() + + m_path.Length(); + + for (const auto& binding : m_textureBindings) { + m_memorySize += binding.name.Length(); + } } } // namespace Resources diff --git a/engine/src/Resources/Mesh/Mesh.cpp b/engine/src/Resources/Mesh/Mesh.cpp index d27acbe4..9b9f7a82 100644 --- a/engine/src/Resources/Mesh/Mesh.cpp +++ b/engine/src/Resources/Mesh/Mesh.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include namespace XCEngine { @@ -11,6 +13,8 @@ void Mesh::Release() { m_vertexData.Clear(); m_indexData.Clear(); m_sections.Clear(); + m_materials.Clear(); + m_textures.Clear(); m_vertexCount = 0; m_vertexStride = 0; m_attributes = VertexAttribute::Position; @@ -52,10 +56,41 @@ void Mesh::AddSection(const MeshSection& section) { UpdateMemorySize(); } +void Mesh::AddMaterial(Material* material) { + m_materials.PushBack(material); + UpdateMemorySize(); +} + +void Mesh::AddTexture(Texture* texture) { + m_textures.PushBack(texture); + UpdateMemorySize(); +} + +Material* Mesh::GetMaterial(Core::uint32 index) const { + if (index >= m_materials.Size()) { + return nullptr; + } + return m_materials[index]; +} + void Mesh::UpdateMemorySize() { m_memorySize = m_vertexData.Size() + m_indexData.Size() + - m_sections.Size() * sizeof(MeshSection); + m_sections.Size() * sizeof(MeshSection) + + m_materials.Size() * sizeof(Material*) + + m_textures.Size() * sizeof(Texture*); + + for (const Material* material : m_materials) { + if (material != nullptr) { + m_memorySize += material->GetMemorySize(); + } + } + + for (const Texture* texture : m_textures) { + if (texture != nullptr) { + m_memorySize += texture->GetMemorySize(); + } + } } } // namespace Resources diff --git a/engine/src/Resources/Mesh/MeshLoader.cpp b/engine/src/Resources/Mesh/MeshLoader.cpp index 5a090537..cc287323 100644 --- a/engine/src/Resources/Mesh/MeshLoader.cpp +++ b/engine/src/Resources/Mesh/MeshLoader.cpp @@ -1,14 +1,21 @@ #include #include +#include +#include +#include #include #include #include +#include +#include #include #include +#include #include #include #include #include +#include #include namespace XCEngine { @@ -23,6 +30,15 @@ struct ImportedMeshData { Math::Bounds bounds; }; +struct TextureImportContext { + const aiScene& scene; + const Containers::String& sourcePath; + std::filesystem::path sourceDirectory; + TextureLoader textureLoader; + std::unordered_map textureCache; + std::vector ownedTextures; +}; + Math::Bounds ComputeBounds(const std::vector& vertices) { if (vertices.empty()) { return Math::Bounds(); @@ -91,6 +107,290 @@ Core::uint32 BuildPostProcessFlags(const MeshImportSettings& settings) { return flags; } +Containers::String ToContainersString(const std::string& value) { + return Containers::String(value.c_str()); +} + +Containers::String BuildSubResourcePath(const Containers::String& sourcePath, + const char* category, + Core::uint32 index, + const Containers::String& extension = Containers::String()) { + std::string path = sourcePath.CStr(); + path += "#"; + path += category; + path += "["; + path += std::to_string(index); + path += "]"; + if (!extension.Empty()) { + path += extension.CStr(); + } + return Containers::String(path.c_str()); +} + +Containers::String GetResourceNameFromPath(const Containers::String& path) { + const std::filesystem::path filePath(path.CStr()); + const std::string fileName = filePath.filename().string(); + if (!fileName.empty()) { + return Containers::String(fileName.c_str()); + } + return path; +} + +Core::uint32 FindEmbeddedTextureIndex(const aiScene& scene, const aiTexture& embeddedTexture) { + for (Core::uint32 index = 0; index < scene.mNumTextures; ++index) { + if (scene.mTextures[index] == &embeddedTexture) { + return index; + } + } + return 0; +} + +Texture* CreateRawTexture(const Containers::String& texturePath, + TextureFormat format, + Core::uint32 width, + Core::uint32 height, + const void* data, + size_t dataSize) { + auto* texture = new Texture(); + + IResource::ConstructParams params; + params.name = GetResourceNameFromPath(texturePath); + params.path = texturePath; + params.guid = ResourceGUID::Generate(texturePath); + params.memorySize = dataSize; + texture->Initialize(params); + + if (!texture->Create(width, + height, + 1, + 1, + TextureType::Texture2D, + format, + data, + dataSize)) { + delete texture; + return nullptr; + } + + return texture; +} + +Texture* LoadEmbeddedTexture(const aiTexture& embeddedTexture, + const Containers::String& texturePath, + TextureImportContext& context) { + const std::string cacheKey(texturePath.CStr()); + const auto cacheIt = context.textureCache.find(cacheKey); + if (cacheIt != context.textureCache.end()) { + return cacheIt->second; + } + + Texture* texture = nullptr; + if (embeddedTexture.mHeight == 0) { + LoadResult result = context.textureLoader.LoadFromMemory(texturePath, + embeddedTexture.pcData, + embeddedTexture.mWidth); + if (result) { + texture = static_cast(result.resource); + } + } else { + const size_t texelCount = static_cast(embeddedTexture.mWidth) * + static_cast(embeddedTexture.mHeight); + std::vector rgbaPixels(texelCount * 4u); + + for (size_t i = 0; i < texelCount; ++i) { + const aiTexel& texel = embeddedTexture.pcData[i]; + rgbaPixels[i * 4 + 0] = texel.r; + rgbaPixels[i * 4 + 1] = texel.g; + rgbaPixels[i * 4 + 2] = texel.b; + rgbaPixels[i * 4 + 3] = texel.a; + } + + texture = CreateRawTexture(texturePath, + TextureFormat::RGBA8_UNORM, + embeddedTexture.mWidth, + embeddedTexture.mHeight, + rgbaPixels.data(), + rgbaPixels.size()); + } + + if (texture != nullptr) { + context.textureCache.emplace(cacheKey, texture); + context.ownedTextures.push_back(texture); + } + + return texture; +} + +Texture* LoadExternalTexture(const std::filesystem::path& textureFilePath, + TextureImportContext& context) { + const std::string normalizedPath = textureFilePath.lexically_normal().string(); + const auto cacheIt = context.textureCache.find(normalizedPath); + if (cacheIt != context.textureCache.end()) { + return cacheIt->second; + } + + LoadResult result = context.textureLoader.Load(Containers::String(normalizedPath.c_str())); + if (!result) { + return nullptr; + } + + Texture* texture = static_cast(result.resource); + context.textureCache.emplace(normalizedPath, texture); + context.ownedTextures.push_back(texture); + return texture; +} + +Texture* LoadTextureReference(const aiString& textureReference, TextureImportContext& context) { + if (textureReference.length == 0) { + return nullptr; + } + + const aiTexture* embeddedTexture = context.scene.GetEmbeddedTexture(textureReference.C_Str()); + if (embeddedTexture != nullptr) { + const Core::uint32 embeddedTextureIndex = FindEmbeddedTextureIndex(context.scene, *embeddedTexture); + std::string extension; + if (embeddedTexture->mHeight == 0 && embeddedTexture->achFormatHint[0] != '\0') { + extension = "."; + extension += embeddedTexture->achFormatHint; + } else { + extension = ".tex"; + } + + const Containers::String texturePath = + BuildSubResourcePath(context.sourcePath, + "embedded_texture", + embeddedTextureIndex, + Containers::String(extension.c_str())); + return LoadEmbeddedTexture(*embeddedTexture, texturePath, context); + } + + std::filesystem::path resolvedPath(textureReference.C_Str()); + if (!resolvedPath.is_absolute()) { + resolvedPath = context.sourceDirectory / resolvedPath; + } + + return LoadExternalTexture(resolvedPath, context); +} + +Texture* LoadMaterialTexture(const aiMaterial& assimpMaterial, + std::initializer_list textureTypes, + TextureImportContext& context) { + for (aiTextureType textureType : textureTypes) { + if (assimpMaterial.GetTextureCount(textureType) == 0) { + continue; + } + + aiString texturePath; + if (assimpMaterial.GetTexture(textureType, 0, &texturePath) != AI_SUCCESS) { + continue; + } + + Texture* texture = LoadTextureReference(texturePath, context); + if (texture != nullptr) { + return texture; + } + } + + return nullptr; +} + +void ImportMaterialProperties(const aiMaterial& assimpMaterial, Material& material) { + float opacity = 1.0f; + if (assimpMaterial.Get(AI_MATKEY_OPACITY, opacity) == AI_SUCCESS) { + material.SetFloat("opacity", opacity); + } + + aiColor4D baseColor; + if (assimpMaterial.Get(AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_BASE_COLOR_FACTOR, baseColor) == AI_SUCCESS) { + material.SetFloat4("baseColor", + Math::Vector4(baseColor.r, baseColor.g, baseColor.b, baseColor.a)); + } else { + aiColor3D diffuseColor; + if (assimpMaterial.Get(AI_MATKEY_COLOR_DIFFUSE, diffuseColor) == AI_SUCCESS) { + material.SetFloat4("baseColor", + Math::Vector4(diffuseColor.r, diffuseColor.g, diffuseColor.b, opacity)); + } + } + + aiColor3D emissiveColor; + if (assimpMaterial.Get(AI_MATKEY_COLOR_EMISSIVE, emissiveColor) == AI_SUCCESS) { + material.SetFloat3("emissiveColor", + Math::Vector3(emissiveColor.r, emissiveColor.g, emissiveColor.b)); + } + + aiColor3D specularColor; + if (assimpMaterial.Get(AI_MATKEY_COLOR_SPECULAR, specularColor) == AI_SUCCESS) { + material.SetFloat3("specularColor", + Math::Vector3(specularColor.r, specularColor.g, specularColor.b)); + } + + float metallic = 0.0f; + if (assimpMaterial.Get(AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLIC_FACTOR, metallic) == AI_SUCCESS) { + material.SetFloat("metallic", metallic); + } + + float roughness = 0.0f; + if (assimpMaterial.Get(AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_ROUGHNESS_FACTOR, roughness) == AI_SUCCESS) { + material.SetFloat("roughness", roughness); + } + + float shininess = 0.0f; + if (assimpMaterial.Get(AI_MATKEY_SHININESS, shininess) == AI_SUCCESS) { + material.SetFloat("shininess", shininess); + } + + int twoSided = 0; + if (assimpMaterial.Get(AI_MATKEY_TWOSIDED, twoSided) == AI_SUCCESS) { + material.SetBool("twoSided", twoSided != 0); + } +} + +void ImportMaterialTextures(const aiMaterial& assimpMaterial, + Material& material, + TextureImportContext& context) { + auto assignTexture = [&](const char* propertyName, + std::initializer_list textureTypes) { + Texture* texture = LoadMaterialTexture(assimpMaterial, textureTypes, context); + if (texture != nullptr) { + material.SetTexture(Containers::String(propertyName), ResourceHandle(texture)); + } + }; + + assignTexture("baseColorTexture", { aiTextureType_BASE_COLOR, aiTextureType_DIFFUSE }); + assignTexture("normalTexture", { aiTextureType_NORMAL_CAMERA, aiTextureType_NORMALS, aiTextureType_HEIGHT }); + assignTexture("specularTexture", { aiTextureType_SPECULAR }); + assignTexture("emissiveTexture", { aiTextureType_EMISSION_COLOR, aiTextureType_EMISSIVE }); + assignTexture("metallicTexture", { aiTextureType_METALNESS }); + assignTexture("roughnessTexture", { aiTextureType_DIFFUSE_ROUGHNESS }); + assignTexture("occlusionTexture", { aiTextureType_AMBIENT_OCCLUSION, aiTextureType_LIGHTMAP }); + assignTexture("opacityTexture", { aiTextureType_OPACITY }); +} + +Material* ImportSingleMaterial(const aiMaterial& assimpMaterial, + const Containers::String& sourcePath, + Core::uint32 materialIndex, + TextureImportContext& context) { + aiString assimpName; + const bool hasName = assimpMaterial.Get(AI_MATKEY_NAME, assimpName) == AI_SUCCESS && + assimpName.length > 0; + const std::string materialName = hasName + ? std::string(assimpName.C_Str()) + : std::string("Material_") + std::to_string(materialIndex); + + auto* material = new Material(); + + IResource::ConstructParams params; + params.name = Containers::String(materialName.c_str()); + params.path = BuildSubResourcePath(sourcePath, "material", materialIndex, Containers::String(".mat")); + params.guid = ResourceGUID::Generate(params.path); + params.memorySize = materialName.length() + params.path.Length(); + material->Initialize(params); + + ImportMaterialProperties(assimpMaterial, *material); + ImportMaterialTextures(assimpMaterial, *material, context); + return material; +} + ImportedMeshData ImportSingleMesh(const aiMesh& mesh, const Math::Matrix4& worldTransform, float globalScale, @@ -282,7 +582,6 @@ LoadResult MeshLoader::Load(const Containers::String& path, const ImportSettings params.path = path; params.guid = ResourceGUID::Generate(path); params.memorySize = 0; - mesh->Initialize(params); mesh->SetVertexData(vertices.data(), @@ -313,6 +612,25 @@ LoadResult MeshLoader::Load(const Containers::String& path, const ImportSettings mesh->AddSection(section); } mesh->SetBounds(ComputeBounds(vertices)); + + if (scene->HasMaterials() && resolvedSettings->HasImportFlag(MeshImportFlags::ImportMaterials)) { + TextureImportContext textureContext{ + *scene, + path, + std::filesystem::path(path.CStr()).parent_path() + }; + + for (Core::uint32 materialIndex = 0; materialIndex < scene->mNumMaterials; ++materialIndex) { + const aiMaterial* assimpMaterial = scene->mMaterials[materialIndex]; + mesh->AddMaterial(assimpMaterial != nullptr + ? ImportSingleMaterial(*assimpMaterial, path, materialIndex, textureContext) + : nullptr); + } + + for (Texture* texture : textureContext.ownedTextures) { + mesh->AddTexture(texture); + } + } return LoadResult(mesh); } diff --git a/engine/src/Resources/Texture/TextureLoader.cpp b/engine/src/Resources/Texture/TextureLoader.cpp index 86cb6ffb..84b3e218 100644 --- a/engine/src/Resources/Texture/TextureLoader.cpp +++ b/engine/src/Resources/Texture/TextureLoader.cpp @@ -1,9 +1,54 @@ #include #include +#include +#include namespace XCEngine { namespace Resources { +namespace { + +Containers::String GetResourceNameFromPath(const Containers::String& path) { + const std::filesystem::path filePath(path.CStr()); + const std::string fileName = filePath.filename().string(); + if (!fileName.empty()) { + return Containers::String(fileName.c_str()); + } + return path; +} + +LoadResult CreateTextureResource(const Containers::String& path, + TextureFormat format, + Core::uint32 width, + Core::uint32 height, + const void* pixelData, + size_t pixelDataSize) { + auto* texture = new Texture(); + + IResource::ConstructParams params; + params.name = GetResourceNameFromPath(path); + params.path = path; + params.guid = ResourceGUID::Generate(path); + params.memorySize = 0; + texture->Initialize(params); + + if (!texture->Create(width, + height, + 1, + 1, + TextureType::Texture2D, + format, + pixelData, + pixelDataSize)) { + delete texture; + return LoadResult(Containers::String("Failed to create texture resource: ") + path); + } + + return LoadResult(texture); +} + +} // namespace + TextureLoader::TextureLoader() = default; TextureLoader::~TextureLoader() = default; @@ -21,8 +66,7 @@ Containers::Array TextureLoader::GetSupportedExtensions() co } bool TextureLoader::CanLoad(const Containers::String& path) const { - Containers::String ext = GetExtension(path); - ext.ToLower(); + Containers::String ext = GetExtension(path).ToLower(); return ext == "png" || ext == "jpg" || ext == "jpeg" || ext == "tga" || ext == "bmp" || ext == "gif" || @@ -32,29 +76,83 @@ bool TextureLoader::CanLoad(const Containers::String& path) const { LoadResult TextureLoader::Load(const Containers::String& path, const ImportSettings* settings) { (void)settings; - Containers::String ext = GetExtension(path); - ext.ToLower(); + Containers::String ext = GetExtension(path).ToLower(); if (!CanLoad(path)) { return LoadResult(Containers::String("Unsupported texture format: ") + ext); } + + if (ext == "dds") { + return LoadResult(Containers::String("DDS texture decoding is not implemented yet: ") + path); + } Containers::Array fileData = ReadFileData(path); if (fileData.Empty()) { return LoadResult(Containers::String("Failed to read file: ") + path); } - - auto* texture = new Texture(); - - IResource::ConstructParams params; - params.name = path; - params.path = path; - params.guid = ResourceGUID::Generate(path); - params.memorySize = fileData.Size(); - - texture->Initialize(params); - - return LoadResult(texture); + + return LoadFromMemory(path, fileData.Data(), fileData.Size()); +} + +LoadResult TextureLoader::LoadFromMemory(const Containers::String& path, const void* data, size_t dataSize) const { + if (data == nullptr || dataSize == 0) { + return LoadResult(Containers::String("Texture data is empty: ") + path); + } + + const auto* stbData = reinterpret_cast(data); + const int stbDataSize = static_cast(dataSize); + + int width = 0; + int height = 0; + int channels = 0; + + if (stbi_is_hdr_from_memory(stbData, stbDataSize) != 0) { + float* pixels = stbi_loadf_from_memory(stbData, + stbDataSize, + &width, + &height, + &channels, + STBI_rgb_alpha); + if (pixels == nullptr) { + return LoadResult(Containers::String("Failed to decode HDR texture: ") + path); + } + + const size_t pixelDataSize = static_cast(width) * + static_cast(height) * + 4u * + sizeof(float); + LoadResult result = CreateTextureResource(path, + TextureFormat::RGBA32_FLOAT, + static_cast(width), + static_cast(height), + pixels, + pixelDataSize); + stbi_image_free(pixels); + return result; + } + + stbi_uc* pixels = stbi_load_from_memory(stbData, + stbDataSize, + &width, + &height, + &channels, + STBI_rgb_alpha); + if (pixels == nullptr) { + return LoadResult(Containers::String("Failed to decode texture: ") + path); + } + + const size_t pixelDataSize = static_cast(width) * + static_cast(height) * + 4u * + sizeof(stbi_uc); + LoadResult result = CreateTextureResource(path, + TextureFormat::RGBA8_UNORM, + static_cast(width), + static_cast(height), + pixels, + pixelDataSize); + stbi_image_free(pixels); + return result; } ImportSettings* TextureLoader::GetDefaultSettings() const { diff --git a/tests/Resources/Material/test_material.cpp b/tests/Resources/Material/test_material.cpp index 9b6d59e5..75a25c06 100644 --- a/tests/Resources/Material/test_material.cpp +++ b/tests/Resources/Material/test_material.cpp @@ -104,6 +104,18 @@ TEST(Material, SetGetTexture) { EXPECT_EQ(material.GetTexture("uDiffuse").Get(), texture); } +TEST(Material, SetTextureReplacesExistingBinding) { + Material material; + Texture* firstTexture = new Texture(); + Texture* secondTexture = new Texture(); + + material.SetTexture("uDiffuse", ResourceHandle(firstTexture)); + material.SetTexture("uDiffuse", ResourceHandle(secondTexture)); + + EXPECT_EQ(material.GetTextureBindingCount(), 1u); + EXPECT_EQ(material.GetTexture("uDiffuse").Get(), secondTexture); +} + TEST(Material, HasProperty) { Material material; diff --git a/tests/Resources/Mesh/test_mesh.cpp b/tests/Resources/Mesh/test_mesh.cpp index a033b223..8a4e83ca 100644 --- a/tests/Resources/Mesh/test_mesh.cpp +++ b/tests/Resources/Mesh/test_mesh.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -31,6 +32,11 @@ TEST(Mesh, GetSections) { EXPECT_EQ(mesh.GetSections().Size(), 0u); } +TEST(Mesh, GetMaterials) { + Mesh mesh; + EXPECT_EQ(mesh.GetMaterials().Size(), 0u); +} + TEST(Mesh, SetVertexAndIndexData) { Mesh mesh; @@ -71,6 +77,17 @@ TEST(Mesh, AddSection) { EXPECT_EQ(mesh.GetSections()[0].materialID, 2u); } +TEST(Mesh, AddMaterial) { + Mesh mesh; + + auto* material = new Material(); + mesh.AddMaterial(material); + + ASSERT_EQ(mesh.GetMaterials().Size(), 1u); + EXPECT_EQ(mesh.GetMaterial(0), material); + EXPECT_GT(mesh.GetMemorySize(), 0u); +} + TEST(Mesh, SetBounds) { Mesh mesh; diff --git a/tests/Resources/Mesh/test_mesh_import_settings.cpp b/tests/Resources/Mesh/test_mesh_import_settings.cpp index e5443515..e24bd309 100644 --- a/tests/Resources/Mesh/test_mesh_import_settings.cpp +++ b/tests/Resources/Mesh/test_mesh_import_settings.cpp @@ -10,7 +10,7 @@ namespace { TEST(MeshImportSettings, DefaultConstructor) { MeshImportSettings settings; - EXPECT_EQ(settings.GetImportFlags(), MeshImportFlags::None); + EXPECT_EQ(settings.GetImportFlags(), MeshImportFlags::ImportMaterials); EXPECT_FLOAT_EQ(settings.GetScale(), 1.0f); EXPECT_EQ(settings.GetOffset(), Vector3::Zero()); EXPECT_TRUE(settings.GetAxisConversion()); diff --git a/tests/Resources/Mesh/test_mesh_loader.cpp b/tests/Resources/Mesh/test_mesh_loader.cpp index 77ebb949..62861042 100644 --- a/tests/Resources/Mesh/test_mesh_loader.cpp +++ b/tests/Resources/Mesh/test_mesh_loader.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include #include @@ -102,4 +104,32 @@ TEST(MeshLoader, GeneratesNormalsAndTangentsWhenRequested) { delete mesh; } +TEST(MeshLoader, ImportsMaterialTexturesFromObj) { + MeshLoader loader; + const std::string path = GetMeshFixturePath("textured_triangle.obj"); + + LoadResult result = loader.Load(path.c_str()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + auto* mesh = static_cast(result.resource); + ASSERT_EQ(mesh->GetSections().Size(), 1u); + ASSERT_GE(mesh->GetMaterials().Size(), 1u); + EXPECT_LT(mesh->GetSections()[0].materialID, mesh->GetMaterials().Size()); + + Material* material = mesh->GetMaterial(mesh->GetSections()[0].materialID); + ASSERT_NE(material, nullptr); + EXPECT_TRUE(material->HasProperty("baseColorTexture")); + EXPECT_EQ(material->GetTextureBindingCount(), 1u); + + ResourceHandle diffuseTexture = material->GetTexture("baseColorTexture"); + ASSERT_TRUE(diffuseTexture.IsValid()); + EXPECT_EQ(diffuseTexture->GetWidth(), 2u); + EXPECT_EQ(diffuseTexture->GetHeight(), 2u); + EXPECT_EQ(diffuseTexture->GetPixelDataSize(), 16u); + EXPECT_EQ(mesh->GetTextures().Size(), 1u); + + delete mesh; +} + } // namespace diff --git a/tests/Resources/Texture/CMakeLists.txt b/tests/Resources/Texture/CMakeLists.txt index 02f673a8..875fbb4b 100644 --- a/tests/Resources/Texture/CMakeLists.txt +++ b/tests/Resources/Texture/CMakeLists.txt @@ -28,5 +28,9 @@ target_include_directories(texture_tests PRIVATE ${CMAKE_SOURCE_DIR}/tests/fixtures ) +target_compile_definitions(texture_tests PRIVATE + XCENGINE_TEST_FIXTURES_DIR="${CMAKE_SOURCE_DIR}/tests/fixtures" +) + include(GoogleTest) gtest_discover_tests(texture_tests) diff --git a/tests/Resources/Texture/test_texture_loader.cpp b/tests/Resources/Texture/test_texture_loader.cpp index bcfb994e..76530979 100644 --- a/tests/Resources/Texture/test_texture_loader.cpp +++ b/tests/Resources/Texture/test_texture_loader.cpp @@ -2,12 +2,17 @@ #include #include #include +#include using namespace XCEngine::Resources; using namespace XCEngine::Containers; namespace { +std::string GetTextureFixturePath(const char* fileName) { + return (std::filesystem::path(XCENGINE_TEST_FIXTURES_DIR) / "Resources" / "Texture" / fileName).string(); +} + TEST(TextureLoader, GetResourceType) { TextureLoader loader; EXPECT_EQ(loader.GetResourceType(), ResourceType::Texture); @@ -34,4 +39,22 @@ TEST(TextureLoader, LoadInvalidPath) { EXPECT_FALSE(result); } +TEST(TextureLoader, LoadValidBmpTexture) { + TextureLoader loader; + const std::string path = GetTextureFixturePath("checker.bmp"); + + LoadResult result = loader.Load(path.c_str()); + ASSERT_TRUE(result); + ASSERT_NE(result.resource, nullptr); + + auto* texture = static_cast(result.resource); + EXPECT_EQ(texture->GetWidth(), 2u); + EXPECT_EQ(texture->GetHeight(), 2u); + EXPECT_EQ(texture->GetTextureType(), TextureType::Texture2D); + EXPECT_EQ(texture->GetFormat(), TextureFormat::RGBA8_UNORM); + EXPECT_EQ(texture->GetPixelDataSize(), 16u); + + delete texture; +} + } // namespace diff --git a/tests/fixtures/Resources/Mesh/checker.bmp b/tests/fixtures/Resources/Mesh/checker.bmp new file mode 100644 index 00000000..a88749d7 Binary files /dev/null and b/tests/fixtures/Resources/Mesh/checker.bmp differ diff --git a/tests/fixtures/Resources/Mesh/textured_triangle.mtl b/tests/fixtures/Resources/Mesh/textured_triangle.mtl new file mode 100644 index 00000000..2884b47a --- /dev/null +++ b/tests/fixtures/Resources/Mesh/textured_triangle.mtl @@ -0,0 +1,7 @@ +newmtl TestMaterial +Ka 1.000000 1.000000 1.000000 +Kd 1.000000 1.000000 1.000000 +Ks 0.000000 0.000000 0.000000 +d 1.0 +illum 2 +map_Kd checker.bmp diff --git a/tests/fixtures/Resources/Mesh/textured_triangle.obj b/tests/fixtures/Resources/Mesh/textured_triangle.obj new file mode 100644 index 00000000..53f0de08 --- /dev/null +++ b/tests/fixtures/Resources/Mesh/textured_triangle.obj @@ -0,0 +1,11 @@ +mtllib textured_triangle.mtl +o TexturedTriangle +v 0.0 0.0 1.0 +v 1.0 0.0 1.0 +v 0.0 1.0 1.0 +vt 0.0 0.0 +vt 1.0 0.0 +vt 0.0 1.0 +vn 0.0 0.0 1.0 +usemtl TestMaterial +f 1/1/1 2/2/1 3/3/1 diff --git a/tests/fixtures/Resources/Texture/checker.bmp b/tests/fixtures/Resources/Texture/checker.bmp new file mode 100644 index 00000000..a88749d7 Binary files /dev/null and b/tests/fixtures/Resources/Texture/checker.bmp differ