#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace XCEngine { namespace Resources { namespace { struct ImportedMeshData { std::vector vertices; std::vector indices; VertexAttribute attributes = VertexAttribute::Position; 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(); } Math::Vector3 min = vertices.front().position; Math::Vector3 max = vertices.front().position; for (const StaticMeshVertex& vertex : vertices) { min.x = std::min(min.x, vertex.position.x); min.y = std::min(min.y, vertex.position.y); min.z = std::min(min.z, vertex.position.z); max.x = std::max(max.x, vertex.position.x); max.y = std::max(max.y, vertex.position.y); max.z = std::max(max.z, vertex.position.z); } Math::Bounds bounds; bounds.SetMinMax(min, max); return bounds; } Math::Matrix4 ConvertAssimpMatrix(const aiMatrix4x4& matrix) { Math::Matrix4 result; result.m[0][0] = matrix.a1; result.m[0][1] = matrix.a2; result.m[0][2] = matrix.a3; result.m[0][3] = matrix.a4; result.m[1][0] = matrix.b1; result.m[1][1] = matrix.b2; result.m[1][2] = matrix.b3; result.m[1][3] = matrix.b4; result.m[2][0] = matrix.c1; result.m[2][1] = matrix.c2; result.m[2][2] = matrix.c3; result.m[2][3] = matrix.c4; result.m[3][0] = matrix.d1; result.m[3][1] = matrix.d2; result.m[3][2] = matrix.d3; result.m[3][3] = matrix.d4; return result; } Core::uint32 BuildPostProcessFlags(const MeshImportSettings& settings) { Core::uint32 flags = aiProcess_Triangulate | aiProcess_JoinIdenticalVertices | aiProcess_SortByPType | aiProcess_ValidateDataStructure | aiProcess_FindInvalidData; if (settings.GetAxisConversion()) { flags |= aiProcess_MakeLeftHanded; flags |= aiProcess_FlipWindingOrder; } if (settings.HasImportFlag(MeshImportFlags::FlipUVs)) { flags |= aiProcess_FlipUVs; } if (settings.HasImportFlag(MeshImportFlags::FlipWindingOrder)) { flags |= aiProcess_FlipWindingOrder; } if (settings.HasImportFlag(MeshImportFlags::GenerateNormals)) { flags |= aiProcess_GenSmoothNormals; } if (settings.HasImportFlag(MeshImportFlags::GenerateTangents)) { flags |= aiProcess_CalcTangentSpace; } if (settings.HasImportFlag(MeshImportFlags::OptimizeMesh)) { flags |= aiProcess_ImproveCacheLocality; flags |= aiProcess_OptimizeMeshes; flags |= aiProcess_OptimizeGraph; } 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()); } TextureImportSettings BuildMaterialTextureImportSettings(const char* propertyName) { TextureImportSettings settings; (void)propertyName; settings.SetSRGB(false); settings.SetTargetFormat(TextureFormat::RGBA8_UNORM); return settings; } std::string BuildTextureCacheKey(const std::string& pathKey, const TextureImportSettings& settings) { std::string cacheKey = pathKey; cacheKey += settings.GetSRGB() ? "|srgb" : "|linear"; cacheKey += "|fmt="; cacheKey += std::to_string(static_cast(settings.GetTargetFormat())); return cacheKey; } 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; } Containers::String NormalizePathString(const std::filesystem::path& path) { return Containers::String(path.lexically_normal().generic_string().c_str()); } Containers::String ResolveArtifactDependencyPath(const Containers::String& dependencyPath, const Containers::String& ownerArtifactPath) { if (dependencyPath.Empty()) { return dependencyPath; } std::filesystem::path dependencyFsPath(dependencyPath.CStr()); if (dependencyFsPath.is_absolute() && std::filesystem::exists(dependencyFsPath)) { return NormalizePathString(dependencyFsPath); } if (std::filesystem::exists(dependencyFsPath)) { return NormalizePathString(dependencyFsPath); } const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot(); if (!resourceRoot.Empty()) { const std::filesystem::path projectRelativeCandidate = std::filesystem::path(resourceRoot.CStr()) / dependencyFsPath; if (std::filesystem::exists(projectRelativeCandidate)) { return NormalizePathString(projectRelativeCandidate); } } const std::filesystem::path ownerArtifactFsPath(ownerArtifactPath.CStr()); if (!ownerArtifactFsPath.is_absolute()) { return dependencyPath; } const std::filesystem::path ownerRelativeCandidate = ownerArtifactFsPath.parent_path() / dependencyFsPath; if (std::filesystem::exists(ownerRelativeCandidate)) { return NormalizePathString(ownerRelativeCandidate); } std::filesystem::path current = ownerArtifactFsPath.parent_path(); while (!current.empty()) { if (current.filename() == "Library") { const std::filesystem::path projectRoot = current.parent_path(); if (!projectRoot.empty()) { const std::filesystem::path projectRelativeCandidate = projectRoot / dependencyFsPath; if (std::filesystem::exists(projectRelativeCandidate)) { return NormalizePathString(projectRelativeCandidate); } } break; } const std::filesystem::path parent = current.parent_path(); if (parent == current) { break; } current = parent; } return dependencyPath; } 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, const TextureImportSettings& settings, TextureImportContext& context) { const std::string cacheKey = BuildTextureCacheKey(texturePath.CStr(), settings); 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, &settings); 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, settings.GetTargetFormat() != TextureFormat::Unknown ? settings.GetTargetFormat() : (settings.GetSRGB() ? TextureFormat::RGBA8_SRGB : 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, const TextureImportSettings& settings, TextureImportContext& context) { const std::string normalizedPath = textureFilePath.lexically_normal().string(); const std::string cacheKey = BuildTextureCacheKey(normalizedPath, settings); const auto cacheIt = context.textureCache.find(cacheKey); if (cacheIt != context.textureCache.end()) { return cacheIt->second; } LoadResult result = context.textureLoader.Load(Containers::String(normalizedPath.c_str()), &settings); if (!result) { return nullptr; } Texture* texture = static_cast(result.resource); context.textureCache.emplace(cacheKey, texture); context.ownedTextures.push_back(texture); return texture; } Texture* LoadTextureReference( const aiString& textureReference, const TextureImportSettings& settings, 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, settings, context); } std::filesystem::path resolvedPath(textureReference.C_Str()); if (!resolvedPath.is_absolute()) { resolvedPath = context.sourceDirectory / resolvedPath; } return LoadExternalTexture(resolvedPath, settings, context); } Texture* LoadMaterialTexture(const aiMaterial& assimpMaterial, std::initializer_list textureTypes, const TextureImportSettings& settings, 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, settings, context); if (texture != nullptr) { return texture; } } return nullptr; } bool HasMaterialTexture( const aiMaterial& assimpMaterial, std::initializer_list textureTypes) { for (aiTextureType textureType : textureTypes) { if (assimpMaterial.GetTextureCount(textureType) > 0) { return true; } } return false; } void ImportMaterialProperties(const aiMaterial& assimpMaterial, Material& material) { float opacity = 1.0f; if (assimpMaterial.Get(AI_MATKEY_OPACITY, opacity) == AI_SUCCESS) { material.SetFloat("opacity", opacity); } const bool hasBaseColorTexture = HasMaterialTexture(assimpMaterial, { aiTextureType_BASE_COLOR, aiTextureType_DIFFUSE }); 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", hasBaseColorTexture ? Math::Vector4(1.0f, 1.0f, 1.0f, opacity) : 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) { const TextureImportSettings settings = BuildMaterialTextureImportSettings(propertyName); Texture* texture = LoadMaterialTexture(assimpMaterial, textureTypes, settings, 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, const Math::Vector3& offset) { ImportedMeshData result; result.vertices.reserve(mesh.mNumVertices); result.indices.reserve(mesh.mNumFaces * 3); VertexAttribute attributes = VertexAttribute::Position; if (mesh.HasNormals()) { attributes = attributes | VertexAttribute::Normal; } if (mesh.HasTangentsAndBitangents()) { attributes = attributes | VertexAttribute::Tangent | VertexAttribute::Bitangent; } if (mesh.HasTextureCoords(0)) { attributes = attributes | VertexAttribute::UV0; } const Math::Matrix4 normalTransform = worldTransform.Inverse().Transpose(); const float appliedScale = globalScale; for (Core::uint32 vertexIndex = 0; vertexIndex < mesh.mNumVertices; ++vertexIndex) { StaticMeshVertex vertex; const aiVector3D& position = mesh.mVertices[vertexIndex]; const Math::Vector3 transformedPosition = worldTransform.MultiplyPoint(Math::Vector3(position.x, position.y, position.z)); vertex.position = transformedPosition * appliedScale + offset; if (mesh.HasNormals()) { const aiVector3D& normal = mesh.mNormals[vertexIndex]; vertex.normal = normalTransform.MultiplyVector(Math::Vector3(normal.x, normal.y, normal.z)).Normalized(); } if (mesh.HasTangentsAndBitangents()) { const aiVector3D& tangent = mesh.mTangents[vertexIndex]; const aiVector3D& bitangent = mesh.mBitangents[vertexIndex]; vertex.tangent = normalTransform.MultiplyVector(Math::Vector3(tangent.x, tangent.y, tangent.z)).Normalized(); vertex.bitangent = normalTransform.MultiplyVector(Math::Vector3(bitangent.x, bitangent.y, bitangent.z)).Normalized(); } if (mesh.HasTextureCoords(0)) { const aiVector3D& uv = mesh.mTextureCoords[0][vertexIndex]; vertex.uv0 = Math::Vector2(uv.x, uv.y); } result.vertices.push_back(vertex); } for (Core::uint32 faceIndex = 0; faceIndex < mesh.mNumFaces; ++faceIndex) { const aiFace& face = mesh.mFaces[faceIndex]; if (face.mNumIndices != 3) { continue; } result.indices.push_back(face.mIndices[0]); result.indices.push_back(face.mIndices[1]); result.indices.push_back(face.mIndices[2]); } result.attributes = attributes; result.bounds = ComputeBounds(result.vertices); return result; } void ProcessNode(const aiNode& node, const aiScene& scene, const Math::Matrix4& parentTransform, const MeshImportSettings& settings, std::vector& vertices, std::vector& indices, Containers::Array& sections, VertexAttribute& attributes) { const Math::Matrix4 worldTransform = parentTransform * ConvertAssimpMatrix(node.mTransformation); const float globalScale = settings.GetScale() * settings.GetImportScale(); for (Core::uint32 meshIndex = 0; meshIndex < node.mNumMeshes; ++meshIndex) { const aiMesh* mesh = scene.mMeshes[node.mMeshes[meshIndex]]; if (mesh == nullptr || mesh->mNumVertices == 0) { continue; } const ImportedMeshData meshData = ImportSingleMesh(*mesh, worldTransform, globalScale, settings.GetOffset()); if (meshData.vertices.empty() || meshData.indices.empty()) { continue; } const Core::uint32 baseVertex = static_cast(vertices.size()); const Core::uint32 startIndex = static_cast(indices.size()); vertices.insert(vertices.end(), meshData.vertices.begin(), meshData.vertices.end()); indices.reserve(indices.size() + meshData.indices.size()); for (Core::uint32 index : meshData.indices) { indices.push_back(baseVertex + index); } MeshSection section{}; section.baseVertex = baseVertex; section.vertexCount = static_cast(meshData.vertices.size()); section.startIndex = startIndex; section.indexCount = static_cast(meshData.indices.size()); section.materialID = mesh->mMaterialIndex; section.bounds = meshData.bounds; sections.PushBack(section); attributes = attributes | meshData.attributes; } for (Core::uint32 childIndex = 0; childIndex < node.mNumChildren; ++childIndex) { const aiNode* child = node.mChildren[childIndex]; if (child != nullptr) { ProcessNode(*child, scene, worldTransform, settings, vertices, indices, sections, attributes); } } } Containers::String ReadBinaryString(std::ifstream& stream) { Core::uint32 length = 0; stream.read(reinterpret_cast(&length), sizeof(length)); if (!stream || length == 0) { return Containers::String(); } std::string buffer(length, '\0'); stream.read(buffer.data(), length); if (!stream) { return Containers::String(); } return Containers::String(buffer.c_str()); } void ApplyMaterialProperty(Material& material, const MaterialProperty& property) { switch (property.type) { case MaterialPropertyType::Float: material.SetFloat(property.name, property.value.floatValue[0]); break; case MaterialPropertyType::Float2: material.SetFloat2(property.name, Math::Vector2(property.value.floatValue[0], property.value.floatValue[1])); break; case MaterialPropertyType::Float3: material.SetFloat3(property.name, Math::Vector3(property.value.floatValue[0], property.value.floatValue[1], property.value.floatValue[2])); break; case MaterialPropertyType::Float4: material.SetFloat4(property.name, Math::Vector4(property.value.floatValue[0], property.value.floatValue[1], property.value.floatValue[2], property.value.floatValue[3])); break; case MaterialPropertyType::Int: material.SetInt(property.name, property.value.intValue[0]); break; case MaterialPropertyType::Bool: material.SetBool(property.name, property.value.boolValue); break; default: break; } } LoadResult LoadMeshArtifact(const Containers::String& path) { std::ifstream input(path.CStr(), std::ios::binary); if (!input.is_open()) { return LoadResult(Containers::String("Failed to read mesh artifact: ") + path); } MeshArtifactHeader header; input.read(reinterpret_cast(&header), sizeof(header)); if (!input) { return LoadResult(Containers::String("Failed to parse mesh artifact header: ") + path); } const std::string magic(header.magic, header.magic + 7); if (magic != "XCMESH2") { return LoadResult(Containers::String("Invalid mesh artifact magic: ") + path); } auto mesh = std::make_unique(); IResource::ConstructParams params; params.name = GetResourceNameFromPath(path); params.path = path; params.guid = ResourceGUID::Generate(path); params.memorySize = 0; mesh->Initialize(params); Containers::Array sections; sections.Resize(header.sectionCount); for (Core::uint32 index = 0; index < header.sectionCount; ++index) { input.read(reinterpret_cast(§ions[index]), sizeof(MeshSection)); if (!input) { return LoadResult(Containers::String("Failed to read mesh sections: ") + path); } } Containers::Array vertexData; vertexData.Resize(static_cast(header.vertexDataSize)); if (header.vertexDataSize > 0) { input.read(reinterpret_cast(vertexData.Data()), static_cast(header.vertexDataSize)); if (!input) { return LoadResult(Containers::String("Failed to read mesh vertex data: ") + path); } } Containers::Array indexData; indexData.Resize(static_cast(header.indexDataSize)); if (header.indexDataSize > 0) { input.read(reinterpret_cast(indexData.Data()), static_cast(header.indexDataSize)); if (!input) { return LoadResult(Containers::String("Failed to read mesh index data: ") + path); } } mesh->SetVertexData(vertexData.Data(), vertexData.Size(), header.vertexCount, header.vertexStride, static_cast(header.vertexAttributes)); mesh->SetIndexData(indexData.Data(), indexData.Size(), header.indexCount, header.use32BitIndex != 0); for (const MeshSection& section : sections) { mesh->AddSection(section); } Math::Bounds bounds; bounds.SetMinMax(header.boundsMin, header.boundsMax); mesh->SetBounds(bounds); MaterialLoader materialLoader; for (Core::uint32 materialIndex = 0; materialIndex < header.materialCount; ++materialIndex) { const Containers::String materialArtifactPath = ReadBinaryString(input); if (!input) { return LoadResult(Containers::String("Failed to read mesh material artifact path: ") + path); } if (materialArtifactPath.Empty()) { mesh->AddMaterial(nullptr); continue; } const Containers::String resolvedMaterialArtifactPath = ResolveArtifactDependencyPath(materialArtifactPath, path); LoadResult materialResult = materialLoader.Load(resolvedMaterialArtifactPath); if (!materialResult || materialResult.resource == nullptr) { return LoadResult( Containers::String("Failed to load mesh material artifact: ") + resolvedMaterialArtifactPath + " for " + path); } auto* material = static_cast(materialResult.resource); material->RecalculateMemorySize(); mesh->AddMaterial(material); } return LoadResult(mesh.release()); } } // namespace MeshLoader::MeshLoader() = default; MeshLoader::~MeshLoader() = default; Containers::Array MeshLoader::GetSupportedExtensions() const { Containers::Array extensions; extensions.PushBack(Containers::String("fbx")); extensions.PushBack(Containers::String("obj")); extensions.PushBack(Containers::String("gltf")); extensions.PushBack(Containers::String("glb")); extensions.PushBack(Containers::String("dae")); extensions.PushBack(Containers::String("stl")); extensions.PushBack(Containers::String("xcmesh")); return extensions; } bool MeshLoader::CanLoad(const Containers::String& path) const { if (IsBuiltinMeshPath(path)) { return true; } Containers::String ext = GetExtension(path).ToLower(); return ext == "fbx" || ext == "obj" || ext == "gltf" || ext == "glb" || ext == "dae" || ext == "stl" || ext == "xcmesh"; } LoadResult MeshLoader::Load(const Containers::String& path, const ImportSettings* settings) { if (IsBuiltinMeshPath(path)) { return CreateBuiltinMeshResource(path); } const Containers::String ext = GetExtension(path).ToLower(); if (!CanLoad(path)) { return LoadResult(Containers::String("Unsupported mesh format: ") + ext); } if (ext == "xcmesh") { return LoadMeshArtifact(path); } Containers::String resolvedPath = path; if (!std::filesystem::path(path.CStr()).is_absolute()) { resolvedPath = ResourceManager::Get().ResolvePath(path); } std::ifstream file(resolvedPath.CStr(), std::ios::binary); if (!file.is_open()) { return LoadResult(Containers::String("Failed to read file: ") + resolvedPath); } MeshImportSettings defaultSettings; const MeshImportSettings* resolvedSettings = dynamic_cast(settings); if (resolvedSettings == nullptr) { resolvedSettings = &defaultSettings; } Assimp::Importer importer; const aiScene* scene = importer.ReadFile(resolvedPath.CStr(), BuildPostProcessFlags(*resolvedSettings)); if (scene == nullptr || scene->mRootNode == nullptr || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) != 0) { const char* errorText = importer.GetErrorString(); return LoadResult(Containers::String("Assimp failed to load mesh: ") + Containers::String(errorText != nullptr ? errorText : "Unknown error")); } std::vector vertices; std::vector indices; Containers::Array sections; VertexAttribute attributes = VertexAttribute::Position; ProcessNode(*scene->mRootNode, *scene, Math::Matrix4::Identity(), *resolvedSettings, vertices, indices, sections, attributes); if (vertices.empty() || indices.empty()) { return LoadResult(Containers::String("No triangle mesh data found in: ") + path); } auto* mesh = new Mesh(); IResource::ConstructParams params; const std::string fileName = std::filesystem::path(resolvedPath.CStr()).filename().string(); params.name = Containers::String(fileName.c_str()); params.path = path; params.guid = ResourceGUID::Generate(path); params.memorySize = 0; mesh->Initialize(params); mesh->SetVertexData(vertices.data(), vertices.size() * sizeof(StaticMeshVertex), static_cast(vertices.size()), sizeof(StaticMeshVertex), attributes); const bool use32BitIndex = vertices.size() > std::numeric_limits::max(); if (use32BitIndex) { mesh->SetIndexData(indices.data(), indices.size() * sizeof(Core::uint32), static_cast(indices.size()), true); } else { std::vector compactIndices; compactIndices.reserve(indices.size()); for (Core::uint32 index : indices) { compactIndices.push_back(static_cast(index)); } mesh->SetIndexData(compactIndices.data(), compactIndices.size() * sizeof(Core::uint16), static_cast(compactIndices.size()), false); } for (const MeshSection& section : sections) { 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); } ImportSettings* MeshLoader::GetDefaultSettings() const { return new MeshImportSettings(); } REGISTER_RESOURCE_LOADER(MeshLoader); } // namespace Resources } // namespace XCEngine