// 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 namespace Nz { namespace { bool IsMD5MeshSupported(const std::string_view& extension) { return (extension == "md5mesh"); } Ternary CheckMD5Mesh(Stream& stream, const MeshParams& parameters) { bool skip; if (parameters.custom.GetBooleanParameter("SkipBuiltinMD5MeshLoader", &skip) && skip) return Ternary::False; MD5MeshParser parser(stream); return parser.Check(); } std::shared_ptr LoadMD5Mesh(Stream& stream, const MeshParams& parameters) { MD5MeshParser parser(stream); if (!parser.Parse()) { NazaraError("MD5Mesh parser failed"); return nullptr; } UInt32 maxWeightCount = 4; long long customMaxWeightCount; if (parameters.custom.GetIntegerParameter("MaxWeightCount", &customMaxWeightCount)) { maxWeightCount = SafeCast(customMaxWeightCount); if (maxWeightCount > 4) { NazaraWarning("MaxWeightCount cannot be over 4"); maxWeightCount = 4; } } // Pour que le squelette soit correctement aligné, il faut appliquer un quaternion "de correction" aux joints à la base du squelette Quaternionf rotationQuat = Quaternionf::RotationBetween(Vector3f::UnitX(), Vector3f::Forward()) * Quaternionf::RotationBetween(Vector3f::UnitZ(), Vector3f::Up()); std::filesystem::path baseDir = stream.GetDirectory(); // Le hellknight de Doom 3 fait ~120 unités, et il est dit qu'il fait trois mètres // Nous réduisons donc la taille générale des fichiers MD5 de 1/40 Matrix4f matrix = Matrix4f::Transform(Nz::Vector3f::Zero(), rotationQuat, Vector3f(1.f / 40.f)); rotationQuat = Quaternionf::Identity(); const MD5MeshParser::Joint* joints = parser.GetJoints(); const MD5MeshParser::Mesh* meshes = parser.GetMeshes(); UInt32 jointCount = parser.GetJointCount(); UInt32 meshCount = parser.GetMeshCount(); if (parameters.animated) { std::shared_ptr mesh = std::make_shared(); mesh->CreateSkeletal(jointCount); Skeleton* skeleton = mesh->GetSkeleton(); for (UInt32 i = 0; i < jointCount; ++i) { Joint* joint = skeleton->GetJoint(i); int parent = joints[i].parent; if (parent >= 0) joint->SetParent(skeleton->GetJoint(parent)); joint->SetInverseBindMatrix(Matrix4f::TransformInverse(joints[i].bindPos, joints[i].bindOrient)); joint->SetName(joints[i].name); } mesh->SetMaterialCount(meshCount); for (UInt32 i = 0; i < meshCount; ++i) { const MD5MeshParser::Mesh& md5Mesh = meshes[i]; UInt64 indexCount = md5Mesh.triangles.size() * 3; UInt64 vertexCount = md5Mesh.vertices.size(); bool largeIndices = (vertexCount > std::numeric_limits::max()); std::shared_ptr indexBuffer = std::make_shared((largeIndices) ? IndexType::U32 : IndexType::U16, indexCount, parameters.indexBufferFlags, parameters.bufferFactory); std::shared_ptr vertexBuffer = std::make_shared(VertexDeclaration::Get(VertexLayout::XYZ_Normal_UV_Tangent_Skinning), UInt32(vertexCount), parameters.vertexBufferFlags, parameters.bufferFactory); // Index buffer IndexMapper indexMapper(*indexBuffer); // Le format définit un set de triangles nous permettant de retrouver facilement les indices // Cependant les sommets des triangles ne sont pas spécifiés dans le même ordre que ceux du moteur // (On parle ici de winding) UInt32 index = 0; for (const MD5MeshParser::Triangle& triangle : md5Mesh.triangles) { // On les respécifie dans le bon ordre (inversion du winding) indexMapper.Set(index++, triangle.x); indexMapper.Set(index++, triangle.z); indexMapper.Set(index++, triangle.y); } indexMapper.Unmap(); if (parameters.optimizeIndexBuffers) indexBuffer->Optimize(); // Vertex buffer struct Weight { float bias; unsigned int jointIndex; }; std::vector tempWeights; VertexMapper vertexMapper(*vertexBuffer); auto posPtr = vertexMapper.GetComponentPtr(VertexComponent::Position); auto jointIndicesPtr = vertexMapper.GetComponentPtr(VertexComponent::JointIndices); auto jointWeightPtr = vertexMapper.GetComponentPtr(VertexComponent::JointWeights); auto uvPtr = vertexMapper.GetComponentPtr(VertexComponent::TexCoord); for (const MD5MeshParser::Vertex& vertex : md5Mesh.vertices) { // Skinning MD5 (Formule d'Id Tech) Vector3f finalPos(Vector3f::Zero()); // On stocke tous les poids dans le tableau temporaire en même temps qu'on calcule la position finale du sommet. tempWeights.resize(vertex.weightCount); for (unsigned int weightIndex = 0; weightIndex < vertex.weightCount; ++weightIndex) { const MD5MeshParser::Weight& weight = md5Mesh.weights[vertex.startWeight + weightIndex]; const MD5MeshParser::Joint& joint = joints[weight.joint]; finalPos += (joint.bindPos + joint.bindOrient * weight.pos) * weight.bias; // Avant d'ajouter les poids, il faut s'assurer qu'il n'y en ait pas plus que le maximum supporté // et dans le cas contraire, garder les poids les plus importants et les renormaliser tempWeights[weightIndex] = {weight.bias, weight.joint}; } // Avons nous plus de poids que le moteur ne peut en supporter ? UInt32 weightCount = vertex.weightCount; if (weightCount > maxWeightCount) { // Pour augmenter la qualité du skinning tout en ne gardant que X poids, on ne garde que les poids // les plus importants, ayant le plus d'impact sur le sommet final std::sort(tempWeights.begin(), tempWeights.end(), [] (const Weight& a, const Weight& b) -> bool { return a.bias > b.bias; }); // Sans oublier bien sûr de renormaliser les poids (que leur somme soit 1) float weightSum = 0.f; for (UInt32 j = 0; j < maxWeightCount; ++j) weightSum += tempWeights[j].bias; for (UInt32 j = 0; j < maxWeightCount; ++j) tempWeights[j].bias /= weightSum; weightCount = maxWeightCount; } if (posPtr) *posPtr++ = finalPos; if (uvPtr) *uvPtr++ = Vector2f(parameters.texCoordOffset + vertex.uv * parameters.texCoordScale); if (jointIndicesPtr) { Vector4i32& jointIndices = *jointIndicesPtr++; for (UInt32 j = 0; j < maxWeightCount; ++j) jointIndices[j] = (j < weightCount) ? tempWeights[j].jointIndex : 0; } if (jointWeightPtr) { Vector4f& jointWeights = *jointWeightPtr++; for (UInt32 j = 0; j < maxWeightCount; ++j) jointWeights[j] = (j < weightCount) ? tempWeights[j].bias : 0; } } // Vertex colors (.md5mesh files have no vertex color) if (auto colorPtr = vertexMapper.GetComponentPtr(VertexComponent::Color)) { for (std::size_t j = 0; j < md5Mesh.vertices.size(); ++j) *colorPtr++ = Color::White; } vertexMapper.Unmap(); // Material ParameterList matData; matData.SetParameter(MaterialData::DiffuseTexturePath, (baseDir / md5Mesh.shader).generic_u8string()); mesh->SetMaterialData(i, std::move(matData)); // Submesh std::shared_ptr subMesh = std::make_shared(vertexBuffer, indexBuffer); if (parameters.vertexDeclaration->HasComponentOfType(VertexComponent::Normal)) { if (parameters.vertexDeclaration->HasComponentOfType(VertexComponent::Tangent)) subMesh->GenerateNormalsAndTangents(); else subMesh->GenerateNormals(); } subMesh->SetMaterialIndex(i); mesh->AddSubMesh(subMesh); // Animation // Il est peut-être éventuellement possible que la probabilité que l'animation ait le même nom soit non-nulle. std::filesystem::path path = stream.GetPath(); if (!path.empty()) { path.replace_extension(".md5anim"); if (std::filesystem::exists(path)) mesh->SetAnimation(path); } } return mesh; } else { std::shared_ptr mesh = std::make_shared(); if (!mesh->CreateStatic()) // Ne devrait jamais échouer { NazaraInternalError("Failed to create mesh"); return nullptr; } mesh->SetMaterialCount(meshCount); for (UInt32 i = 0; i < meshCount; ++i) { const MD5MeshParser::Mesh& md5Mesh = meshes[i]; UInt64 indexCount = md5Mesh.triangles.size() * 3; UInt64 vertexCount = md5Mesh.vertices.size(); // Index buffer bool largeIndices = (vertexCount > std::numeric_limits::max()); std::shared_ptr indexBuffer = std::make_shared((largeIndices) ? IndexType::U32 : IndexType::U16, indexCount, parameters.indexBufferFlags, parameters.bufferFactory); IndexMapper indexMapper(*indexBuffer); IndexIterator index = indexMapper.begin(); for (const MD5MeshParser::Triangle& triangle : md5Mesh.triangles) { // On les respécifie dans le bon ordre *index++ = triangle.x; *index++ = triangle.z; *index++ = triangle.y; } indexMapper.Unmap(); if (parameters.optimizeIndexBuffers) indexBuffer->Optimize(); // Vertex buffer std::shared_ptr vertexBuffer = std::make_shared(parameters.vertexDeclaration, vertexCount, parameters.vertexBufferFlags, parameters.bufferFactory); VertexMapper vertexMapper(*vertexBuffer); // Vertex positions if (auto posPtr = vertexMapper.GetComponentPtr(VertexComponent::Position)) { for (const MD5MeshParser::Vertex& md5Vertex : md5Mesh.vertices) { // Id Tech MD5 skinning Vector3f finalPos(Vector3f::Zero()); for (unsigned int j = 0; j < md5Vertex.weightCount; ++j) { const MD5MeshParser::Weight& weight = md5Mesh.weights[md5Vertex.startWeight + j]; const MD5MeshParser::Joint& joint = joints[weight.joint]; finalPos += (joint.bindPos + joint.bindOrient * weight.pos) * weight.bias; } // On retourne le modèle dans le bon sens *posPtr++ = matrix * finalPos; } } // Vertex UVs if (auto uvPtr = vertexMapper.GetComponentPtr(VertexComponent::TexCoord)) { for (const MD5MeshParser::Vertex& md5Vertex : md5Mesh.vertices) *uvPtr++ = parameters.texCoordOffset + md5Vertex.uv * parameters.texCoordScale; } // Vertex colors (.md5mesh files have no vertex color) if (auto colorPtr = vertexMapper.GetComponentPtr(VertexComponent::Color)) { for (std::size_t j = 0; j < md5Mesh.vertices.size(); ++j) *colorPtr++ = Color::White; } vertexMapper.Unmap(); // Submesh std::shared_ptr subMesh = std::make_shared(vertexBuffer, indexBuffer); subMesh->GenerateAABB(); subMesh->SetMaterialIndex(i); if (parameters.vertexDeclaration->HasComponentOfType(VertexComponent::Normal)) { if (parameters.vertexDeclaration->HasComponentOfType(VertexComponent::Tangent)) subMesh->GenerateNormalsAndTangents(); else subMesh->GenerateNormals(); } mesh->AddSubMesh(subMesh); // Material ParameterList matData; matData.SetParameter(MaterialData::DiffuseTexturePath, (baseDir / md5Mesh.shader).generic_u8string()); mesh->SetMaterialData(i, std::move(matData)); } if (parameters.center) mesh->Recenter(); return mesh; } } } namespace Loaders { MeshLoader::Entry GetMeshLoader_MD5Mesh() { MeshLoader::Entry loader; loader.extensionSupport = IsMD5MeshSupported; loader.streamChecker = CheckMD5Mesh; loader.streamLoader = LoadMD5Mesh; return loader; } } }