// Copyright (C) 2022 Jérôme "Lynix" Leclercq (lynix680@gmail.com) // This file is part of the "Nazara Engine - Utility module" // For conditions of distribution and use, see copyright notice in Config.hpp #include #include #include #include #include #include #include #include #include #include #include #include #include ///TODO: N'avoir qu'un seul VertexBuffer communs à tous les meshes namespace Nz { namespace { bool IsSupported(const std::string_view& extension) { return (extension == "obj"); } Ternary Check(Stream& stream, const MeshParams& parameters) { NazaraUnused(stream); bool skip; if (parameters.custom.GetBooleanParameter("SkipNativeOBJLoader", &skip) && skip) return Ternary::False; OBJParser parser; if (!parser.Check(stream)) return Ternary::False; return Ternary::Unknown; } bool ParseMTL(Mesh& mesh, const std::filesystem::path& filePath, const std::string* materials, const OBJParser::Mesh* meshes, std::size_t meshCount) { File file(filePath); if (!file.Open(OpenMode::ReadOnly | OpenMode::Text)) { NazaraError("Failed to open MTL file (" + file.GetPath().generic_u8string() + ')'); return false; } MTLParser materialParser; if (!materialParser.Parse(file)) { NazaraError("MTL parser failed"); return false; } std::unordered_map materialCache; std::filesystem::path baseDir = file.GetDirectory(); for (std::size_t i = 0; i < meshCount; ++i) { const std::string& matName = materials[meshes[i].material]; const MTLParser::Material* mtlMat = materialParser.GetMaterial(matName); if (!mtlMat) { NazaraWarning("MTL has no material \"" + matName + '"'); continue; } auto it = materialCache.find(matName); if (it == materialCache.end()) { ParameterList data; UInt8 alphaValue = static_cast(mtlMat->alpha*255.f); Color ambientColor(mtlMat->ambient); Color diffuseColor(mtlMat->diffuse); Color specularColor(mtlMat->specular); ambientColor.a = alphaValue; diffuseColor.a = alphaValue; specularColor.a = alphaValue; data.SetParameter(MaterialData::AmbientColor, ambientColor); data.SetParameter(MaterialData::DiffuseColor, diffuseColor); data.SetParameter(MaterialData::Shininess, mtlMat->shininess); data.SetParameter(MaterialData::SpecularColor, specularColor); if (!mtlMat->alphaMap.empty()) { std::filesystem::path fullPath = mtlMat->alphaMap; if (!fullPath.is_absolute()) fullPath = baseDir / fullPath; data.SetParameter(MaterialData::AlphaTexturePath, fullPath.generic_u8string()); } if (!mtlMat->diffuseMap.empty()) { std::filesystem::path fullPath = mtlMat->diffuseMap; if (!fullPath.is_absolute()) fullPath = baseDir / fullPath; data.SetParameter(MaterialData::DiffuseTexturePath, fullPath.generic_u8string()); } if (!mtlMat->emissiveMap.empty()) { std::filesystem::path fullPath = mtlMat->emissiveMap; if (!fullPath.is_absolute()) fullPath = baseDir / fullPath; data.SetParameter(MaterialData::EmissiveTexturePath, fullPath.generic_u8string()); } if (!mtlMat->normalMap.empty()) { std::filesystem::path fullPath = mtlMat->normalMap; if (!fullPath.is_absolute()) fullPath = baseDir / fullPath; data.SetParameter(MaterialData::NormalTexturePath, fullPath.generic_u8string()); } if (!mtlMat->specularMap.empty()) { std::filesystem::path fullPath = mtlMat->specularMap; if (!fullPath.is_absolute()) fullPath = baseDir / fullPath; data.SetParameter(MaterialData::SpecularTexturePath, fullPath.generic_u8string()); } // If we either have an alpha value or an alpha map, let's configure the material for transparency if (alphaValue != 255 || !mtlMat->alphaMap.empty()) { // Some default settings data.SetParameter(MaterialData::Blending, true); data.SetParameter(MaterialData::DepthWrite, true); data.SetParameter(MaterialData::BlendDstAlpha, static_cast(BlendFunc::Zero)); data.SetParameter(MaterialData::BlendDstColor, static_cast(BlendFunc::InvSrcAlpha)); data.SetParameter(MaterialData::BlendModeAlpha, static_cast(BlendEquation::Add)); data.SetParameter(MaterialData::BlendModeColor, static_cast(BlendEquation::Add)); data.SetParameter(MaterialData::BlendSrcAlpha, static_cast(BlendFunc::One)); data.SetParameter(MaterialData::BlendSrcColor, static_cast(BlendFunc::SrcAlpha)); } it = materialCache.emplace(matName, std::move(data)).first; } mesh.SetMaterialData(meshes[i].material, it->second); } return true; } std::shared_ptr Load(Stream& stream, const MeshParams& parameters) { long long reservedVertexCount; if (!parameters.custom.GetIntegerParameter("NativeOBJLoader_VertexCount", &reservedVertexCount)) reservedVertexCount = 100; OBJParser parser; if (!parser.Parse(stream, reservedVertexCount)) { NazaraError("OBJ parser failed"); return nullptr; } std::shared_ptr mesh = std::make_shared(); mesh->CreateStatic(); const std::string* materials = parser.GetMaterials(); const Vector4f* positions = parser.GetPositions(); const Vector3f* normals = parser.GetNormals(); const Vector3f* texCoords = parser.GetTexCoords(); const OBJParser::Mesh* meshes = parser.GetMeshes(); std::size_t meshCount = parser.GetMeshCount(); NazaraAssert(materials != nullptr && positions != nullptr && normals != nullptr && texCoords != nullptr && meshes != nullptr && meshCount > 0, "Invalid OBJParser output"); // Triangulation temporary vector std::vector faceIndices; for (std::size_t i = 0; i < meshCount; ++i) { std::size_t faceCount = meshes[i].faces.size(); if (faceCount == 0) continue; std::vector indices; indices.reserve(faceCount*3); // Pire cas si les faces sont des triangles // Afin d'utiliser OBJParser::FaceVertex comme clé dans un unordered_map, // nous devons fournir un foncteur de hash ainsi qu'un foncteur de comparaison // Hash struct FaceVertexHasher { std::size_t operator()(const OBJParser::FaceVertex& o) const { std::size_t seed = 0; HashCombine(seed, o.normal); HashCombine(seed, o.position); HashCombine(seed, o.texCoord); return seed; } }; // Comparaison struct FaceVertexComparator { bool operator()(const OBJParser::FaceVertex& lhs, const OBJParser::FaceVertex& rhs) const { return lhs.normal == rhs.normal && lhs.position == rhs.position && lhs.texCoord == rhs.texCoord; } }; std::unordered_map vertices; vertices.reserve(meshes[i].vertices.size()); UInt32 vertexCount = 0; for (unsigned int j = 0; j < faceCount; ++j) { std::size_t faceVertexCount = meshes[i].faces[j].vertexCount; faceIndices.resize(faceVertexCount); for (std::size_t k = 0; k < faceVertexCount; ++k) { const OBJParser::FaceVertex& vertex = meshes[i].vertices[meshes[i].faces[j].firstVertex + k]; auto it = vertices.find(vertex); if (it == vertices.end()) it = vertices.emplace(vertex, vertexCount++).first; faceIndices[k] = it->second; } // Triangulation for (std::size_t k = 1; k < faceVertexCount-1; ++k) { indices.push_back(faceIndices[0]); indices.push_back(faceIndices[k]); indices.push_back(faceIndices[k+1]); } } // Création des buffers std::shared_ptr indexBuffer = std::make_shared(vertexCount > std::numeric_limits::max(), indices.size(), parameters.indexBufferFlags, parameters.bufferFactory); std::shared_ptr vertexBuffer = std::make_shared(parameters.vertexDeclaration, vertexCount, parameters.vertexBufferFlags, parameters.bufferFactory); // Remplissage des indices IndexMapper indexMapper(*indexBuffer); for (std::size_t j = 0; j < indices.size(); ++j) indexMapper.Set(j, indices[j]); indexMapper.Unmap(); // Pour laisser les autres tâches affecter l'index buffer if (parameters.optimizeIndexBuffers) indexBuffer->Optimize(); // Remplissage des vertices // Make sure the normal matrix won't rescale our normals Nz::Matrix4f normalMatrix = parameters.matrix; if (normalMatrix.HasScale()) normalMatrix.ApplyScale(1.f / normalMatrix.GetScale()); bool hasNormals = true; bool hasTexCoords = true; VertexMapper vertexMapper(*vertexBuffer); auto normalPtr = vertexMapper.GetComponentPtr(VertexComponent::Normal); auto posPtr = vertexMapper.GetComponentPtr(VertexComponent::Position); auto uvPtr = vertexMapper.GetComponentPtr(VertexComponent::TexCoord); if (!normalPtr) hasNormals = false; if (!uvPtr) hasTexCoords = false; for (auto& vertexPair : vertices) { const OBJParser::FaceVertex& vertexIndices = vertexPair.first; unsigned int index = vertexPair.second; if (posPtr) { const Vector4f& vec = positions[vertexIndices.position - 1]; posPtr[index] = Vector3f(parameters.matrix * vec); } if (hasNormals) { if (vertexIndices.normal > 0) normalPtr[index] = normalMatrix.Transform(normals[vertexIndices.normal - 1], 0.f); else hasNormals = false; } if (hasTexCoords) { if (vertexIndices.texCoord > 0) { Vector2f uv = Vector2f(texCoords[vertexIndices.texCoord - 1]); uv.y = 1.f - uv.y; //< OBJ model texcoords seems to majority start from bottom left uvPtr[index] = Vector2f(parameters.texCoordOffset + uv * parameters.texCoordScale); } else hasTexCoords = false; } } // Official .obj files have no vertex color, fill it with white if (auto colorPtr = vertexMapper.GetComponentPtr(VertexComponent::Color)) { for (UInt32 j = 0; j < vertexCount; ++j) colorPtr[j] = Color::White; } vertexMapper.Unmap(); std::shared_ptr subMesh = std::make_shared(std::move(vertexBuffer), indexBuffer); subMesh->GenerateAABB(); subMesh->SetMaterialIndex(meshes[i].material); // Ce que nous pouvons générer dépend des données à disposition (par exemple les tangentes nécessitent des coordonnées de texture) if (hasNormals && hasTexCoords) subMesh->GenerateTangents(); else if (hasTexCoords) subMesh->GenerateNormalsAndTangents(); else if (normalPtr) subMesh->GenerateNormals(); mesh->AddSubMesh(meshes[i].name + '_' + materials[meshes[i].material], subMesh); } mesh->SetMaterialCount(parser.GetMaterialCount()); if (parameters.center) mesh->Recenter(); // On charge les matériaux si demandé std::filesystem::path mtlLib = parser.GetMtlLib(); if (!mtlLib.empty()) { ErrorFlags flags(ErrorMode::ThrowExceptionDisabled); ParseMTL(*mesh, stream.GetDirectory() / mtlLib, materials, meshes, meshCount); } return mesh; } } namespace Loaders { MeshLoader::Entry GetMeshLoader_OBJ() { MeshLoader::Entry loader; loader.extensionSupport = IsSupported; loader.streamChecker = Check; loader.streamLoader = Load; return loader; } } }