00e860f326
- Invalidate 'Physics Materials From Mesh' boolean from collider component - Removed material library from material selector. Default material library will always be used instead. - Marking failing automated test as xfail - Added default material to physics configuration. - Moved material library asset from physx configuration to physics configuration, as it doesn't need to be physx specific. - Refactor physics material system having into account that there is only one material library in the project. - Renaming code from DefaultMaterialLibrary to MaterialLibrary. - All queries about physics materials unified under PhysicsMaterialRequests bus. - PhysXSystem only manages the material library asset. - Saving and reloading the same physics material asset with different content didn't trigger a events that the material library has changed. - Changing Physics Material Request interface to use shared_ptr instead of weak_ptr to be simpler to handle the returned materials and having a more consistent code. - Refactored Material Manager to improve its implementation. Still following the same approach of "creating materials on the fly as they are requested", but now it's doing it consistently across the interface, with private helpers functions FindOrCreateMaterial that simplify vastly the implementation. - Material Manager now listens to change event of material library asset and default material configuration so it updates its materials accordingly. - Complete Material move constructor and operator.
891 lines
41 KiB
C++
891 lines
41 KiB
C++
/*
|
|
* 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 <PhysX_precompiled.h>
|
|
|
|
#include <AzFramework/StringFunc/StringFunc.h>
|
|
#include <AzToolsFramework/Debug/TraceContext.h>
|
|
#include <SceneAPI/SceneCore/Containers/Utilities/Filters.h>
|
|
#include <SceneAPI/SceneCore/Containers/Utilities/SceneGraphUtilities.h>
|
|
#include <SceneAPI/SceneCore/DataTypes/GraphData/IMeshData.h>
|
|
#include <SceneAPI/SceneCore/Events/ExportEventContext.h>
|
|
#include <SceneAPI/SceneCore/Events/ExportProductList.h>
|
|
#include <SceneAPI/SceneCore/Utilities/FileUtilities.h>
|
|
#include <SceneAPI/SceneCore/Utilities/Reporting.h>
|
|
#include <SceneAPI/SceneCore/Containers/Views/SceneGraphChildIterator.h>
|
|
#include <SceneAPI/SceneCore/DataTypes/GraphData/IMaterialData.h>
|
|
|
|
#include <PhysX/MeshAsset.h>
|
|
#include <Source/Material.h>
|
|
#include <Source/Pipeline/MeshAssetHandler.h>
|
|
#include <Source/Pipeline/MeshExporter.h>
|
|
#include <Source/Pipeline/PrimitiveShapeFitter/PrimitiveShapeFitter.h>
|
|
#include <Source/Pipeline/MeshGroup.h>
|
|
#include <Source/Utils.h>
|
|
|
|
#include <PxPhysicsAPI.h>
|
|
#include <VHACD.h>
|
|
|
|
#include <Cry_Math.h>
|
|
#include <MathConversion.h>
|
|
#include <AzCore/IO/SystemFile.h>
|
|
#include <AzCore/Math/Matrix3x3.h>
|
|
#include <AzCore/XML/rapidxml.h>
|
|
#include <AzCore/std/algorithm.h>
|
|
#include <GFxFramework/MaterialIO/Material.h>
|
|
|
|
// 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([[maybe_unused]] physx::PxErrorCode::Enum code, [[maybe_unused]] const char* message, [[maybe_unused]] const char* file, [[maybe_unused]] 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<AZStd::string> m_fbxMaterialNames;
|
|
|
|
// Look-up table for fbxMaterialNames
|
|
AZStd::unordered_map<AZStd::string, size_t> m_materialIndexByName;
|
|
};
|
|
|
|
// A struct to store the geometry data per FBX node
|
|
struct NodeCollisionGeomExportData
|
|
{
|
|
AZStd::vector<Vec3> m_vertices;
|
|
AZStd::vector<vtx_idx> m_indices;
|
|
AZStd::vector<AZ::u16> 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([[maybe_unused]] 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<AZ::SerializeContext*>(context);
|
|
if (serializeContext)
|
|
{
|
|
serializeContext->Class<MeshExporter, AZ::SceneAPI::SceneCore::ExportingComponent>()->Version(4);
|
|
}
|
|
}
|
|
|
|
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<AZStd::string>& fbxMaterialNames = materials.m_fbxMaterialNames;
|
|
AZStd::unordered_map<AZStd::string, size_t>& 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<AZStd::string, AZStd::string>& 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<char> 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<char> document;
|
|
document.parse<AZ::rapidxml::parse_no_data_nodes>(buffer.data());
|
|
|
|
//Parse MTL file for materials and/or submaterials.
|
|
AZ::rapidxml::xml_node<char>* rootMaterialNode = document.first_node(AZ::GFxFramework::MaterialExport::g_materialString);
|
|
|
|
AZ::rapidxml::xml_node<char>* subMaterialNode = rootMaterialNode->first_node(AZ::GFxFramework::MaterialExport::g_subMaterialString);
|
|
|
|
if (subMaterialNode)
|
|
{
|
|
for (AZ::rapidxml::xml_node<char>* materialNode = subMaterialNode->first_node(AZ::GFxFramework::MaterialExport::g_materialString);
|
|
materialNode;
|
|
materialNode = materialNode->next_sibling(AZ::GFxFramework::MaterialExport::g_materialString))
|
|
{
|
|
AZ::rapidxml::xml_attribute<char>* nameAttribute = materialNode->first_attribute(AZ::GFxFramework::MaterialExport::g_nameString);
|
|
if (nameAttribute)
|
|
{
|
|
AZStd::string materialName = nameAttribute->value();
|
|
AZStd::string surfaceTypeName = DefaultPhysicsMaterialNameFromPhysicsAsset;
|
|
|
|
AZ::rapidxml::xml_attribute<char>* 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<AZStd::string>& fbxMaterialNames,
|
|
const AZStd::unordered_map<AZStd::string, AZStd::string>& materialToSurfaceTypeMap,
|
|
MeshAssetData& assetData)
|
|
{
|
|
AZStd::vector<AZStd::string>& 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 = DefaultPhysicsMaterialNameFromPhysicsAsset;
|
|
}
|
|
|
|
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<physx::PxU8*>(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<physx::PxU8*>(assetData), assetDataSize);
|
|
physx::PxConvexMesh* convexMesh = PxGetPhysics().createConvexMesh(inpStream);
|
|
|
|
bool success = convexMesh != nullptr;
|
|
convexMesh->release();
|
|
return success;
|
|
}
|
|
|
|
AZStd::vector<AZStd::string> GenerateLocalNodeMaterialMap(const AZ::SceneAPI::Containers::SceneGraph& graph, const AZ::SceneAPI::Containers::SceneGraph::NodeIndex& nodeIndex)
|
|
{
|
|
AZStd::vector<AZStd::string> materialNames;
|
|
|
|
auto view = AZ::SceneAPI::Containers::Views::MakeSceneGraphChildView<AZ::SceneAPI::Containers::Views::AcceptEndPointsOnly>(
|
|
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 == "android" || 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<AZ::u16>& faceMaterials)
|
|
{
|
|
AZStd::unordered_set<AZ::u16> 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
|
|
bool CookPhysXMesh(
|
|
const AZStd::vector<Vec3>& vertices,
|
|
const AZStd::vector<AZ::u32>& indices,
|
|
const AZStd::vector<AZ::u16>& faceMaterials,
|
|
AZStd::vector<AZ::u8>* 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<NodeCollisionGeomExportData>& 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<AZStd::string, AZStd::string> 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<AZ::u8> physxData;
|
|
bool success = CookPhysXMesh(subMesh.m_vertices, subMesh.m_indices, subMesh.m_perFaceMaterialIndices,
|
|
&(physxData), meshGroup, context.GetPlatformIdentifier());
|
|
|
|
if (success)
|
|
{
|
|
AZStd::shared_ptr<Physics::CookedMeshShapeConfiguration> shapeConfig =
|
|
AZStd::make_shared<Physics::CookedMeshShapeConfiguration>();
|
|
|
|
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<AZStd::string>();
|
|
AZ::Uuid productUuid = AZ::Uuid::CreateData(productUuidString.data(), productUuidString.size() * sizeof(productUuidString[0]));
|
|
|
|
context.GetProductList().AddProduct(AZStd::move(filename), productUuid, AZ::AzTypeInfo<MeshAsset>::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<NodeCollisionGeomExportData>& 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<float> 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<decltype(nodeExportData.m_indices)::value_type, uint32_t>::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<uint32_t> 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<float>(vertex[0]),
|
|
static_cast<float>(vertex[1]),
|
|
static_cast<float>(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<AZ::u16>(
|
|
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<MeshGroup>(valueStorage);
|
|
|
|
ScopedVHACD decomposer;
|
|
|
|
for (const MeshGroup& pxMeshGroup : view)
|
|
{
|
|
// Export data per node
|
|
AZStd::vector<NodeCollisionGeomExportData> 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);
|
|
}
|
|
|
|
for (size_t index = 0; index < selectedNodeCount; index++)
|
|
{
|
|
AZ::SceneAPI::Containers::SceneGraph::NodeIndex nodeIndex = graph.Find(sceneNodeSelectionList.GetSelectedNode(index));
|
|
auto nodeMesh = azrtti_cast<const AZ::SceneAPI::DataTypes::IMeshData*>(*graph.ConvertToStorageIterator(nodeIndex));
|
|
|
|
if (!nodeMesh)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const AZ::SceneAPI::Containers::SceneGraph::Name& nodeName = graph.GetNodeName(nodeIndex);
|
|
|
|
const AZStd::vector<AZStd::string> localFbxMaterialsList = Utils::GenerateLocalNodeMaterialMap(graph, nodeIndex);
|
|
const AZ::SceneAPI::DataTypes::MatrixType worldTransform = 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
|
|
);
|
|
}
|
|
|
|
// Convex and primitive methods can only have 1 material
|
|
const bool limitToOneMaterial = pxMeshGroup.GetExportAsConvex() || pxMeshGroup.GetExportAsPrimitive();
|
|
|
|
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];
|
|
|
|
// Keep using the first material when it has to be limited to one.
|
|
if (limitToOneMaterial &&
|
|
assetMaterialData.m_fbxMaterialNames.size() == 1 &&
|
|
assetMaterialData.m_fbxMaterialNames[0] != materialName)
|
|
{
|
|
materialName = assetMaterialData.m_fbxMaterialNames[0];
|
|
}
|
|
}
|
|
|
|
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<Vec3>& mergedVertices = mergedData.m_vertices;
|
|
AZStd::vector<vtx_idx>& mergedIndices = mergedData.m_indices;
|
|
AZStd::vector<AZ::u16>& 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();
|
|
}
|
|
}
|
|
}
|