/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #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 #include #include #include #include // A utility macro helping set/clear bits in a single line #define SET_BITS(flags, condition, bits) flags = (condition) ? ((flags) | (bits)) : ((flags) & ~(bits)) namespace PhysX { namespace Pipeline { namespace SceneContainers = AZ::SceneAPI::Containers; namespace SceneEvents = AZ::SceneAPI::Events; namespace SceneUtil = AZ::SceneAPI::Utilities; static physx::PxDefaultAllocator pxDefaultAllocatorCallback; static const char* const DefaultMaterialName = "default"; // Implementation of the PhysX error callback interface directing errors to ErrorWindow output. static class PxExportErrorCallback : public physx::PxErrorCallback { public: void reportError(physx::PxErrorCode::Enum code, const char* message, const char* file, int line) override final { AZ_TracePrintf(AZ::SceneAPI::Utilities::ErrorWindow, "PxErrorCode %i: %s (line %i in %s)", code, message, line, file); } } pxDefaultErrorCallback; // A struct to store the asset-wide material names shared by multiple shapes struct AssetMaterialsData { // Material names coming from FBX, these will be Mesh Surfaces in the Collider Component AZStd::vector m_fbxMaterialNames; // Look-up table for fbxMaterialNames AZStd::unordered_map m_materialIndexByName; }; // A struct to store the geometry data per FBX node struct NodeCollisionGeomExportData { AZStd::vector m_vertices; AZStd::vector m_indices; AZStd::vector m_perFaceMaterialIndices; AZStd::string m_nodeName; }; // Implementation of the V-HACD log callback interface directing all messages to LogWindow output. static class VHACDLogCallback : public VHACD::IVHACD::IUserLogger { void Log(const char* const msg) override final { AZ_TracePrintf(AZ::SceneAPI::Utilities::LogWindow, "V-HACD: %s", msg); } } vhacdDefaultLogCallback; // Scope-guarded interface to VHACD library. Uses lazy initialization and RAII to free resources upon deletion. class ScopedVHACD { public: ScopedVHACD() : vhacdPtr(nullptr) { } ~ScopedVHACD() { if (vhacdPtr) { vhacdPtr->Clean(); vhacdPtr->Release(); } } ScopedVHACD(ScopedVHACD const&) = delete; ScopedVHACD& operator=(ScopedVHACD const&) = delete; VHACD::IVHACD& operator*() { return *GetImplementation(); } VHACD::IVHACD* operator->() { return GetImplementation(); } private: VHACD::IVHACD* GetImplementation() { if (!vhacdPtr) { vhacdPtr = VHACD::CreateVHACD(); AZ_Assert(vhacdPtr, "Failed to create VHACD instance."); } return vhacdPtr; } VHACD::IVHACD* vhacdPtr; }; MeshExporter::MeshExporter() { BindToCall(&MeshExporter::ProcessContext); } void MeshExporter::Reflect(AZ::ReflectContext* context) { AZ::SerializeContext* serializeContext = azrtti_cast(context); if (serializeContext) { serializeContext->Class()->Version(3); } } namespace Utils { // Utility function doing look-up in fbxMaterialNames and inserting the name if it's not found AZ::u16 InsertMaterialIndexByName(const AZStd::string& materialName, AssetMaterialsData& materials) { AZStd::vector& fbxMaterialNames = materials.m_fbxMaterialNames; AZStd::unordered_map& materialIndexByName = materials.m_materialIndexByName; // Check if we have this material in the list auto materialIndexIter = materialIndexByName.find(materialName); if (materialIndexIter != materialIndexByName.end()) { return materialIndexIter->second; } // Add it to the list otherwise fbxMaterialNames.push_back(materialName); AZ::u16 newIndex = fbxMaterialNames.size() - 1; materialIndexByName[materialName] = newIndex; return newIndex; } // Building a map between FBX material name and the corresponding Cry surface type that is set in the .mtl file. void BuildMaterialToSurfaceTypeMap(const AZStd::string& materialFilename, AZStd::unordered_map& materialToSurfaceTypeMap) { AZ::IO::SystemFile mtlFile; bool fileOpened = mtlFile.Open(materialFilename.c_str(), AZ::IO::SystemFile::SF_OPEN_READ_ONLY); if (fileOpened && mtlFile.Length() != 0) { //Read material override file into a buffer AZStd::vector buffer(mtlFile.Length()); mtlFile.Read(mtlFile.Length(), buffer.data()); mtlFile.Close(); //Apparently in rapidxml if 'parse_no_data_nodes' isn't set it creates both value and data nodes //with the data nodes having precedence such that updating values doesn't work. AZ::rapidxml::xml_document document; document.parse(buffer.data()); //Parse MTL file for materials and/or submaterials. AZ::rapidxml::xml_node* rootMaterialNode = document.first_node(AZ::GFxFramework::MaterialExport::g_materialString); AZ::rapidxml::xml_node* subMaterialNode = rootMaterialNode->first_node(AZ::GFxFramework::MaterialExport::g_subMaterialString); if (subMaterialNode) { for (AZ::rapidxml::xml_node* materialNode = subMaterialNode->first_node(AZ::GFxFramework::MaterialExport::g_materialString); materialNode; materialNode = materialNode->next_sibling(AZ::GFxFramework::MaterialExport::g_materialString)) { AZ::rapidxml::xml_attribute* nameAttribute = materialNode->first_attribute(AZ::GFxFramework::MaterialExport::g_nameString); if (nameAttribute) { AZStd::string materialName = nameAttribute->value(); AZStd::string surfaceTypeName = DefaultMaterialName; AZ::rapidxml::xml_attribute* surfaceTypeNode = materialNode->first_attribute("SurfaceType"); if (surfaceTypeNode && surfaceTypeNode->value_size() != 0) { surfaceTypeName = surfaceTypeNode->value(); } materialToSurfaceTypeMap[materialName] = surfaceTypeName; } else { AZ_TracePrintf(AZ::SceneAPI::Utilities::ErrorWindow, "A SubMaterial without Name found in the .mtl file: %s", materialFilename.c_str()); } } } else { AZ_TracePrintf(AZ::SceneAPI::Utilities::ErrorWindow, "No SubMaterial node in the .mtl file: %s", materialFilename.c_str()); } } } void UpdateAssetMaterialsFromCrySurfaceTypes(const AZStd::vector& fbxMaterialNames, const AZStd::unordered_map& materialToSurfaceTypeMap, MeshAssetData& assetData) { AZStd::vector& materialNames = assetData.m_materialNames; AZ_Assert(materialNames.empty(), "UpdateAssetMaterialsFromCrySurfaceTypes: Mesh Asset Data should not have materials already assigned."); materialNames.clear(); materialNames.reserve(fbxMaterialNames.size()); for (const AZStd::string& fbxMaterial : fbxMaterialNames) { AZStd::string materialName; // Here we assign the actual engine surface type based on the material name auto materialToSurfaceIt = materialToSurfaceTypeMap.find(fbxMaterial); if (materialToSurfaceIt != materialToSurfaceTypeMap.end() && !materialToSurfaceIt->second.empty()) { materialName = materialToSurfaceIt->second; // Remove the mat_ prefix since the material library generated from surface types doesn't have it. if (materialName.find("mat_") == 0) { materialName = materialName.substr(4); } } else { materialName = DefaultMaterialName; } materialNames.emplace_back(AZStd::move(materialName)); } // Asset mesh surfaces match FBX materials. These are the names that users see in the Collider Component in the Editor. assetData.m_surfaceNames = fbxMaterialNames; } bool ValidateCookedTriangleMesh(void* assetData, AZ::u32 assetDataSize) { physx::PxDefaultMemoryInputData inpStream(static_cast(assetData), assetDataSize); physx::PxTriangleMesh* triangleMesh = PxGetPhysics().createTriangleMesh(inpStream); bool success = triangleMesh != nullptr; triangleMesh->release(); return success; } bool ValidateCookedConvexMesh(void* assetData, AZ::u32 assetDataSize) { physx::PxDefaultMemoryInputData inpStream(static_cast(assetData), assetDataSize); physx::PxConvexMesh* convexMesh = PxGetPhysics().createConvexMesh(inpStream); bool success = convexMesh != nullptr; convexMesh->release(); return success; } AZStd::vector GenerateLocalNodeMaterialMap(const AZ::SceneAPI::Containers::SceneGraph& graph, const AZ::SceneAPI::Containers::SceneGraph::NodeIndex& nodeIndex) { AZStd::vector materialNames; auto view = AZ::SceneAPI::Containers::Views::MakeSceneGraphChildView( graph, nodeIndex, graph.GetContentStorage().begin(), true ); for (auto it = view.begin(), itEnd = view.end(); it != itEnd; ++it) { if ((*it) && (*it)->RTTI_IsTypeOf(AZ::SceneAPI::DataTypes::IMaterialData::TYPEINFO_Uuid())) { AZStd::string nodeName = graph.GetNodeName(graph.ConvertToNodeIndex(it.GetHierarchyIterator())).GetName(); materialNames.push_back(nodeName); } } return materialNames; } } static physx::PxMeshMidPhase::Enum GetMidPhaseStructureType(const AZStd::string& platformIdentifier) { // Use by default 3.4 since 3.3 is being deprecated (despite being default) physx::PxMeshMidPhase::Enum ret = physx::PxMeshMidPhase::eBVH34; // Fallback to 3.3 on Android and iOS platforms since they don't support SSE2, which is required for 3.4 if (platformIdentifier == "es3" || platformIdentifier == "ios") { ret = physx::PxMeshMidPhase::eBVH33; } return ret; } // Checks that the entire mesh is assigned (at most) one material (required for convexes and primitives). static void RequireSingleFaceMaterial(const AZStd::vector& faceMaterials) { AZStd::unordered_set uniqueFaceMaterials(faceMaterials.begin(), faceMaterials.end()); if (uniqueFaceMaterials.size() > 1) { AZ_TracePrintf(AZ::SceneAPI::Utilities::WarningWindow, "Should only have 1 material assigned to a non-triangle mesh. Assigned: %d", uniqueFaceMaterials.size()); } } // Cooks the geometry provided into a memory buffer based on the rules set in MeshGroup // Note: This function is also used by CGFMeshAssetBuilderWorker.cpp to generate .pxmesh from cgf bool CookPhysXMesh( const AZStd::vector& vertices, const AZStd::vector& indices, const AZStd::vector& faceMaterials, AZStd::vector* output, const MeshGroup& meshGroup, const AZStd::string& platformIdentifier) { bool cookingSuccessful = false; AZStd::string cookingResultErrorCodeString; const ConvexAssetParams& convexAssetParams = meshGroup.GetConvexAssetParams(); const TriangleMeshAssetParams& triangleMeshAssetParams = meshGroup.GetTriangleMeshAssetParams(); bool shouldExportAsConvex = meshGroup.GetExportAsConvex(); physx::PxCookingParams pxCookingParams = physx::PxCookingParams(physx::PxTolerancesScale()); pxCookingParams.buildGPUData = false; pxCookingParams.midphaseDesc.setToDefault(GetMidPhaseStructureType(platformIdentifier)); if (shouldExportAsConvex) { if (convexAssetParams.GetCheckZeroAreaTriangles()) { pxCookingParams.areaTestEpsilon = convexAssetParams.GetAreaTestEpsilon(); } pxCookingParams.planeTolerance = convexAssetParams.GetPlaneTolerance(); pxCookingParams.gaussMapLimit = convexAssetParams.GetGaussMapLimit(); } else { pxCookingParams.midphaseDesc.mBVH34Desc.numPrimsPerLeaf = triangleMeshAssetParams.GetNumTrisPerLeaf(); pxCookingParams.meshWeldTolerance = triangleMeshAssetParams.GetMeshWeldTolerance(); pxCookingParams.buildTriangleAdjacencies = triangleMeshAssetParams.GetBuildTriangleAdjacencies(); pxCookingParams.suppressTriangleMeshRemapTable = triangleMeshAssetParams.GetSuppressTriangleMeshRemapTable(); if (triangleMeshAssetParams.GetWeldVertices()) { pxCookingParams.meshPreprocessParams |= physx::PxMeshPreprocessingFlag::eWELD_VERTICES; } if (triangleMeshAssetParams.GetDisableCleanMesh()) { pxCookingParams.meshPreprocessParams |= physx::PxMeshPreprocessingFlag::eDISABLE_CLEAN_MESH; } if (triangleMeshAssetParams.GetForce32BitIndices()) { pxCookingParams.meshPreprocessParams |= physx::PxMeshPreprocessingFlag::eFORCE_32BIT_INDICES; } } physx::PxCooking* pxCooking = PxCreateCooking(PX_PHYSICS_VERSION, PxGetFoundation(), pxCookingParams); AZ_Assert(pxCooking, "Failed to create PxCooking"); physx::PxBoundedData strideData; strideData.count = vertices.size(); strideData.stride = sizeof(Vec3); strideData.data = vertices.data(); physx::PxDefaultMemoryOutputStream cookedMeshData; if (shouldExportAsConvex) { physx::PxConvexMeshDesc convexDesc; convexDesc.points = strideData; convexDesc.flags = physx::PxConvexFlag::eCOMPUTE_CONVEX; SET_BITS(convexDesc.flags, convexAssetParams.GetUse16bitIndices(), physx::PxConvexFlag::e16_BIT_INDICES); SET_BITS(convexDesc.flags, convexAssetParams.GetCheckZeroAreaTriangles(), physx::PxConvexFlag::eCHECK_ZERO_AREA_TRIANGLES); SET_BITS(convexDesc.flags, convexAssetParams.GetQuantizeInput(), physx::PxConvexFlag::eQUANTIZE_INPUT); SET_BITS(convexDesc.flags, convexAssetParams.GetUsePlaneShifting(), physx::PxConvexFlag::ePLANE_SHIFTING); SET_BITS(convexDesc.flags, convexAssetParams.GetBuildGpuData(), physx::PxConvexFlag::eGPU_COMPATIBLE); SET_BITS(convexDesc.flags, convexAssetParams.GetShiftVertices(), physx::PxConvexFlag::eSHIFT_VERTICES); physx::PxConvexMeshCookingResult::Enum convexCookingResultCode = physx::PxConvexMeshCookingResult::eSUCCESS; cookingSuccessful = pxCooking->cookConvexMesh(convexDesc, cookedMeshData, &convexCookingResultCode) && Utils::ValidateCookedConvexMesh(cookedMeshData.getData(), cookedMeshData.getSize()); cookingResultErrorCodeString = PhysX::Utils::ConvexCookingResultToString(convexCookingResultCode); // Check how many unique materials are assigned onto the convex mesh. // Report it to the user if there's more than 1 since PhysX only supports a single material assigned to a convex RequireSingleFaceMaterial(faceMaterials); } else { physx::PxTriangleMeshDesc meshDesc; meshDesc.points = strideData; meshDesc.triangles.count = indices.size() / 3; meshDesc.triangles.stride = sizeof(AZ::u32) * 3; meshDesc.triangles.data = indices.data(); meshDesc.materialIndices.stride = sizeof(AZ::u16); meshDesc.materialIndices.data = faceMaterials.data(); physx::PxTriangleMeshCookingResult::Enum trimeshCookingResultCode = physx::PxTriangleMeshCookingResult::eSUCCESS; cookingSuccessful = pxCooking->cookTriangleMesh(meshDesc, cookedMeshData, &trimeshCookingResultCode) && Utils::ValidateCookedTriangleMesh(cookedMeshData.getData(), cookedMeshData.getSize()); cookingResultErrorCodeString = PhysX::Utils::TriMeshCookingResultToString(trimeshCookingResultCode); } if (cookingSuccessful) { output->insert(output->end(), cookedMeshData.getData(), cookedMeshData.getData() + cookedMeshData.getSize()); } else { AZ_TracePrintf(AZ::SceneAPI::Utilities::ErrorWindow, "Cooking Mesh failed: %s", cookingResultErrorCodeString.c_str()); } pxCooking->release(); return cookingSuccessful; } // Utility function finding out the .mtl file for a given FBX (at the moment it's the same name as FBX but with .mtl extension) static AZStd::string GetAssetMaterialFilename(const AZ::SceneAPI::Events::ExportEventContext& context) { const AZ::SceneAPI::Containers::Scene& scene = context.GetScene(); AZStd::string materialFilename = scene.GetSourceFilename(); AzFramework::StringFunc::Path::ReplaceExtension(materialFilename, ".mtl"); return materialFilename; } // Processes the collected data and writes into a file static AZ::SceneAPI::Events::ProcessingResult WritePxMeshAsset( AZ::SceneAPI::Events::ExportEventContext& context, const AZStd::vector& totalExportData, const AssetMaterialsData &assetMaterialsData, const MeshGroup& meshGroup) { SceneEvents::ProcessingResult result = SceneEvents::ProcessingResult::Ignored; AZStd::string assetName = meshGroup.GetName(); AZStd::string filename = SceneUtil::FileUtilities::CreateOutputFileName(assetName, context.GetOutputDirectory(), MeshAssetHandler::s_assetFileExtension); MeshAssetData assetData; const AZStd::string& materialFilename = GetAssetMaterialFilename(context); // Read the information about surface type for each material from the .mtl file AZStd::unordered_map fbxMaterialToCrySurfaceTypeMap; Utils::BuildMaterialToSurfaceTypeMap(materialFilename, fbxMaterialToCrySurfaceTypeMap); // Assign the materials into cooked data Utils::UpdateAssetMaterialsFromCrySurfaceTypes(assetMaterialsData.m_fbxMaterialNames, fbxMaterialToCrySurfaceTypeMap, assetData); for (const NodeCollisionGeomExportData& subMesh : totalExportData) { MeshAssetData::ShapeConfigurationPair shape; if (meshGroup.GetExportAsPrimitive()) { // Only one material can be assigned to a primitive collider, so report a warning if the mesh has // multiple materials assigned to it. RequireSingleFaceMaterial(subMesh.m_perFaceMaterialIndices); const PrimitiveAssetParams& primitiveAssetParams = meshGroup.GetPrimitiveAssetParams(); shape = FitPrimitiveShape( subMesh.m_nodeName, subMesh.m_vertices, primitiveAssetParams.GetVolumeTermCoefficient(), primitiveAssetParams.GetPrimitiveShapeTarget() ); } else { // Cook the mesh into a binary buffer. AZStd::vector physxData; bool success = CookPhysXMesh(subMesh.m_vertices, subMesh.m_indices, subMesh.m_perFaceMaterialIndices, &(physxData), meshGroup, context.GetPlatformIdentifier()); if (success) { AZStd::shared_ptr shapeConfig = AZStd::make_shared(); shapeConfig->SetCookedMeshData( physxData.data(), physxData.size(), meshGroup.GetExportAsConvex() ? Physics::CookedMeshShapeConfiguration::MeshType::Convex : Physics::CookedMeshShapeConfiguration::MeshType::TriangleMesh ); shape.second = shapeConfig; } else { AZ_TracePrintf(AZ::SceneAPI::Utilities::ErrorWindow, "Mesh cooking terminated unsuccessfully."); } } if (shape.second) { assetData.m_colliderShapes.push_back(shape); } else { AZ_TracePrintf( AZ::SceneAPI::Utilities::ErrorWindow, "WritePxMeshAsset: Failed to create asset. Node: %s", subMesh.m_nodeName.c_str() ); return SceneEvents::ProcessingResult::Failure; } if (meshGroup.GetExportAsTriMesh()) { assetData.m_materialIndexPerShape.push_back(MeshAssetData::TriangleMeshMaterialIndex); } else { AZ_Assert( !subMesh.m_perFaceMaterialIndices.empty(), "WritePxMeshAsset: m_perFaceMaterialIndices must be not empty! Please make sure you have a material assigned to the geometry. Node: %s", subMesh.m_nodeName.c_str() ); AZ_Assert( subMesh.m_perFaceMaterialIndices[0] != MeshAssetData::TriangleMeshMaterialIndex, "WritePxMeshAsset: m_perFaceMaterialIndices has invalid material index! Node: %s", subMesh.m_nodeName.c_str() ); assetData.m_materialIndexPerShape.push_back(subMesh.m_perFaceMaterialIndices[0]); } } if (PhysX::Utils::WriteCookedMeshToFile(filename, assetData)) { AZStd::string productUuidString = meshGroup.GetId().ToString(); AZ::Uuid productUuid = AZ::Uuid::CreateData(productUuidString.data(), productUuidString.size() * sizeof(productUuidString[0])); context.GetProductList().AddProduct(AZStd::move(filename), productUuid, AZ::AzTypeInfo::Uuid(), AZStd::nullopt, AZStd::nullopt); result = SceneEvents::ProcessingResult::Success; } else { AZ_TracePrintf(AZ::SceneAPI::Utilities::ErrorWindow, "Unable to write to a file for a PhysX mesh asset. AssetName: %s, filename: %s", assetName.c_str(), filename.c_str()); result = SceneEvents::ProcessingResult::Failure; } return result; } void DecomposeAndAppendMeshes( ScopedVHACD& decomposer, VHACD::IVHACD::Parameters vhacdParams, AZStd::vector& totalExportData, const NodeCollisionGeomExportData& nodeExportData ) { RequireSingleFaceMaterial(nodeExportData.m_perFaceMaterialIndices); AZ_Assert( !nodeExportData.m_perFaceMaterialIndices.empty(), "DecomposeAndAppendMeshes: Empty per-face material vector. Node: %s", nodeExportData.m_nodeName.c_str() ); decomposer->Clean(); // Convert the vertices to a float array suitable for passing to V-HACD. AZStd::vector vhacdVertices; vhacdVertices.reserve(nodeExportData.m_vertices.size() * 3); for (const Vec3& vertex : nodeExportData.m_vertices) { vhacdVertices.emplace_back(vertex[0]); vhacdVertices.emplace_back(vertex[1]); vhacdVertices.emplace_back(vertex[2]); } if constexpr (AZStd::is_same::value) { decomposer->Compute( vhacdVertices.data(), vhacdVertices.size() / 3, nodeExportData.m_indices.data(), nodeExportData.m_indices.size() / 3, vhacdParams ); } else { // Convert the indices to an unsigned 32-bit integer array suitable for passing to V-HACD. AZStd::vector vhacdIndices; vhacdIndices.reserve(nodeExportData.m_indices.size()); for (auto index : nodeExportData.m_indices) { vhacdIndices.emplace_back(index); } decomposer->Compute( vhacdVertices.data(), vhacdVertices.size() / 3, vhacdIndices.data(), vhacdIndices.size() / 3, vhacdParams ); } const AZ::u32 numberOfHulls = decomposer->GetNConvexHulls(); AZ_Assert(numberOfHulls > 0, "V-HACD returned no convex hulls."); AZ_TracePrintf(AZ::SceneAPI::Utilities::LogWindow, "Convex decomposition returned %d hulls", numberOfHulls); for (AZ::u32 hullCounter = 0; hullCounter < numberOfHulls; ++hullCounter) { VHACD::IVHACD::ConvexHull convexHull; decomposer->GetConvexHull(hullCounter, convexHull); NodeCollisionGeomExportData nodeExportDataConvexPart; // Copy vertices. nodeExportDataConvexPart.m_vertices.reserve(convexHull.m_nPoints); for (AZ::u32 vertexCounter = 0; vertexCounter < convexHull.m_nPoints; ++vertexCounter) { double* vertex = convexHull.m_points + 3 * vertexCounter; nodeExportDataConvexPart.m_vertices.emplace_back(Vec3( static_cast(vertex[0]), static_cast(vertex[1]), static_cast(vertex[2]) )); } // Copy indices. nodeExportDataConvexPart.m_indices.reserve(convexHull.m_nTriangles * 3); for (AZ::u32 indexCounter = 0; indexCounter < convexHull.m_nTriangles * 3; ++indexCounter) { nodeExportDataConvexPart.m_indices.emplace_back(convexHull.m_triangles[indexCounter]); } // Set up single per-face material. nodeExportDataConvexPart.m_perFaceMaterialIndices = AZStd::vector( convexHull.m_nTriangles, nodeExportData.m_perFaceMaterialIndices[0] ); nodeExportDataConvexPart.m_nodeName = nodeExportData.m_nodeName + "_" + AZStd::to_string(hullCounter); totalExportData.emplace_back(AZStd::move(nodeExportDataConvexPart)); } } SceneEvents::ProcessingResult MeshExporter::ProcessContext(SceneEvents::ExportEventContext& context) const { AZ_TraceContext("Exporter", "PhysX"); SceneEvents::ProcessingResultCombiner result; const AZ::SceneAPI::Containers::Scene& scene = context.GetScene(); const AZ::SceneAPI::Containers::SceneGraph& graph = scene.GetGraph(); const SceneContainers::SceneManifest& manifest = context.GetScene().GetManifest(); SceneContainers::SceneManifest::ValueStorageConstData valueStorage = manifest.GetValueStorage(); auto view = SceneContainers::MakeExactFilterView(valueStorage); ScopedVHACD decomposer; for (const MeshGroup& pxMeshGroup : view) { // Export data per node AZStd::vector totalExportData; AssetMaterialsData assetMaterialData; const AZStd::string& groupName = pxMeshGroup.GetName(); AZ_TraceContext("Group Name", groupName); const auto& sceneNodeSelectionList = pxMeshGroup.GetSceneNodeSelectionList(); size_t selectedNodeCount = sceneNodeSelectionList.GetSelectedNodeCount(); // Setup VHACD parameters if required. VHACD::IVHACD::Parameters vhacdParams; if (pxMeshGroup.GetDecomposeMeshes()) { const ConvexDecompositionParams& convexDecompositionParams = pxMeshGroup.GetConvexDecompositionParams(); vhacdParams.m_callback = nullptr; vhacdParams.m_logger = &vhacdDefaultLogCallback; vhacdParams.m_concavity = convexDecompositionParams.GetConcavity(); vhacdParams.m_alpha = convexDecompositionParams.GetAlpha(); vhacdParams.m_beta = convexDecompositionParams.GetBeta(); vhacdParams.m_minVolumePerCH = convexDecompositionParams.GetMinVolumePerConvexHull(); vhacdParams.m_resolution = convexDecompositionParams.GetResolution(); vhacdParams.m_maxNumVerticesPerCH = convexDecompositionParams.GetMaxNumVerticesPerConvexHull(); vhacdParams.m_planeDownsampling = convexDecompositionParams.GetPlaneDownsampling(); vhacdParams.m_convexhullDownsampling = convexDecompositionParams.GetConvexHullDownsampling(); vhacdParams.m_maxConvexHulls = convexDecompositionParams.GetMaxConvexHulls(); vhacdParams.m_pca = convexDecompositionParams.GetPca(); vhacdParams.m_mode = convexDecompositionParams.GetMode(); vhacdParams.m_projectHullVertices = convexDecompositionParams.GetProjectHullVertices(); } else { totalExportData.reserve(selectedNodeCount); } auto originRule = pxMeshGroup.GetRuleContainerConst().FindFirstByType(); AZ::Transform originTransform = originRule ? SceneUtil::GetOriginRuleTransform(*originRule, context.GetScene().GetGraph()) : AZ::Transform::CreateIdentity(); for (size_t index = 0; index < selectedNodeCount; index++) { AZ::SceneAPI::Containers::SceneGraph::NodeIndex nodeIndex = graph.Find(sceneNodeSelectionList.GetSelectedNode(index)); auto nodeMesh = azrtti_cast(*graph.ConvertToStorageIterator(nodeIndex)); if (!nodeMesh) { continue; } const AZ::SceneAPI::Containers::SceneGraph::Name& nodeName = graph.GetNodeName(nodeIndex); const AZStd::vector localFbxMaterialsList = Utils::GenerateLocalNodeMaterialMap(graph, nodeIndex); const AZ::Transform worldTransform = originTransform * SceneUtil::BuildWorldTransform(graph, nodeIndex); NodeCollisionGeomExportData nodeExportData; nodeExportData.m_nodeName = nodeName.GetName(); const AZ::u32 vertexCount = nodeMesh->GetVertexCount(); const AZ::u32 faceCount = nodeMesh->GetFaceCount(); nodeExportData.m_vertices.resize(vertexCount); for (AZ::u32 vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex) { AZ::Vector3 pos = nodeMesh->GetPosition(vertexIndex); pos = worldTransform * pos; nodeExportData.m_vertices[vertexIndex] = AZVec3ToLYVec3(pos); } nodeExportData.m_indices.resize(faceCount * 3); nodeExportData.m_perFaceMaterialIndices.resize(faceCount); if (localFbxMaterialsList.empty()) { AZ_TracePrintf( AZ::SceneAPI::Utilities::WarningWindow, "Node '%s' does not have any material assigned to it. Material '%s' will be used.", nodeExportData.m_nodeName.c_str(), DefaultMaterialName ); } for (AZ::u32 faceIndex = 0; faceIndex < faceCount; ++faceIndex) { AZStd::string materialName = DefaultMaterialName; if (!localFbxMaterialsList.empty()) { int materialId = nodeMesh->GetFaceMaterialId(faceIndex); if (materialId >= localFbxMaterialsList.size()) { AZ_TracePrintf(AZ::SceneAPI::Utilities::ErrorWindow, "materialId %d for face %d is out of bound for localFbxMaterialsList (size %d).", materialId, faceIndex, localFbxMaterialsList.size()); return SceneEvents::ProcessingResult::Failure; } materialName = localFbxMaterialsList[materialId]; } const AZ::SceneAPI::DataTypes::IMeshData::Face& face = nodeMesh->GetFaceInfo(faceIndex); nodeExportData.m_indices[faceIndex * 3] = face.vertexIndex[0]; nodeExportData.m_indices[faceIndex * 3 + 1] = face.vertexIndex[1]; nodeExportData.m_indices[faceIndex * 3 + 2] = face.vertexIndex[2]; AZ::u16 materialIndex = Utils::InsertMaterialIndexByName(materialName, assetMaterialData); nodeExportData.m_perFaceMaterialIndices[faceIndex] = materialIndex; } if (pxMeshGroup.GetDecomposeMeshes()) { DecomposeAndAppendMeshes(decomposer, vhacdParams, totalExportData, nodeExportData); } else { totalExportData.emplace_back(AZStd::move(nodeExportData)); } } // Merge triangle meshes if there's more than 1 if (pxMeshGroup.GetExportAsTriMesh() && pxMeshGroup.GetTriangleMeshAssetParams().GetMergeMeshes() && totalExportData.size() > 1) { NodeCollisionGeomExportData mergedData; mergedData.m_nodeName = groupName; AZStd::vector& mergedVertices = mergedData.m_vertices; AZStd::vector& mergedIndices = mergedData.m_indices; AZStd::vector& mergedPerFaceMaterials = mergedData.m_perFaceMaterialIndices; // Here we add the geometry data for each node into a single merged one // Vertices & materials can be added directly but indices need to be incremented // by the amount of vertices already added in the last iteration for (const NodeCollisionGeomExportData& exportData : totalExportData) { vtx_idx startingIndex = mergedVertices.size(); mergedVertices.insert(mergedVertices.end(), exportData.m_vertices.begin(), exportData.m_vertices.end()); mergedPerFaceMaterials.insert(mergedPerFaceMaterials.end(), exportData.m_perFaceMaterialIndices.begin(), exportData.m_perFaceMaterialIndices.end()); mergedIndices.reserve(mergedIndices.size() + exportData.m_indices.size()); AZStd::transform(exportData.m_indices.begin(), exportData.m_indices.end(), AZStd::back_inserter(mergedIndices), [startingIndex](vtx_idx index) { return index + startingIndex; }); } // Clear the data per node and use only the merged one totalExportData.clear(); totalExportData.emplace_back(AZStd::move(mergedData)); } if (!totalExportData.empty()) { result += WritePxMeshAsset(context, totalExportData, assetMaterialData, pxMeshGroup); } } return result.GetResult(); } } }