浏览代码

Added ability to store per triangle user data in mesh shape (#1225)

Use MeshShapeSettings::mPerTriangleUserData at about 25% memory increase to get per triangle user data through MeshShape::GetTriangleUserData
Added Shape::GetLeafShape function to be able to get a leaf shape given a sub shape ID
Added example and unit test
Jorrit Rouwe 11 月之前
父节点
当前提交
d7f08b8367

+ 3 - 3
Jolt/AABBTree/AABBTreeToBuffer.h

@@ -37,7 +37,7 @@ public:
 	static const int TriangleHeaderSize = TriangleCodec::TriangleHeaderSize;
 
 	/// Convert AABB tree. Returns false if failed.
-	bool							Convert(const VertexList &inVertices, const AABBTreeBuilder::Node *inRoot, const char *&outError)
+	bool							Convert(const VertexList &inVertices, const AABBTreeBuilder::Node *inRoot, bool inStoreUserData, const char *&outError)
 	{
 		const typename NodeCodec::EncodingContext node_ctx;
 		typename TriangleCodec::EncodingContext tri_ctx(inVertices);
@@ -46,7 +46,7 @@ public:
 		uint tri_count = inRoot->GetTriangleCountInTree();
 		uint node_count = inRoot->GetNodeCount();
 		uint nodes_size = node_ctx.GetPessimisticMemoryEstimate(node_count);
-		uint total_size = HeaderSize + TriangleHeaderSize + nodes_size + tri_ctx.GetPessimisticMemoryEstimate(tri_count);
+		uint total_size = HeaderSize + TriangleHeaderSize + nodes_size + tri_ctx.GetPessimisticMemoryEstimate(tri_count, inStoreUserData);
 		mTree.reserve(total_size);
 
 		// Reset counters
@@ -157,7 +157,7 @@ public:
 				else
 				{
 					// Add triangles
-					node_data->mTriangleStart = tri_ctx.Pack(node_data->mNode->mTriangles, mTree, outError);
+					node_data->mTriangleStart = tri_ctx.Pack(node_data->mNode->mTriangles, inStoreUserData, mTree, outError);
 					if (node_data->mTriangleStart == uint(-1))
 						return false;
 				}

+ 50 - 9
Jolt/AABBTree/TriangleCodec/TriangleCodecIndexed8BitPackSOA4Flags.h

@@ -13,6 +13,7 @@ JPH_NAMESPACE_BEGIN
 /// TriangleBlockHeader,
 /// TriangleBlock (4 triangles and their flags in 16 bytes),
 /// TriangleBlock...
+/// [Optional] UserData (4 bytes per triangle)
 ///
 /// Vertices are stored:
 ///
@@ -77,13 +78,22 @@ public:
 
 	static_assert(sizeof(TriangleBlock) == 16, "Compiler added padding");
 
+	enum ETriangleBlockHeaderFlags : uint32
+	{
+		OFFSET_TO_VERTICES_BITS = 29,							///< Offset from current block to start of vertices in bytes
+		OFFSET_TO_VERTICES_MASK = (1 << OFFSET_TO_VERTICES_BITS) - 1,
+		OFFSET_TO_USERDATA_BITS = 3,							///< When user data is stored, this is the number of blocks to skip to get to the user data (0 = no user data)
+		OFFSET_TO_USERDATA_MASK = (1 << OFFSET_TO_USERDATA_BITS) - 1,
+	};
+
 	/// A triangle header, will be followed by one or more TriangleBlocks
 	struct TriangleBlockHeader
 	{
-		const VertexData *			GetVertexData() const		{ return reinterpret_cast<const VertexData *>(reinterpret_cast<const uint8 *>(this) + mOffsetToVertices); }
+		const VertexData *			GetVertexData() const		{ return reinterpret_cast<const VertexData *>(reinterpret_cast<const uint8 *>(this) + (mFlags & OFFSET_TO_VERTICES_MASK)); }
 		const TriangleBlock *		GetTriangleBlock() const	{ return reinterpret_cast<const TriangleBlock *>(reinterpret_cast<const uint8 *>(this) + sizeof(TriangleBlockHeader)); }
+		const uint32 *				GetUserData() const			{ uint32 offset = mFlags >> OFFSET_TO_VERTICES_BITS; return offset == 0? nullptr : reinterpret_cast<const uint32 *>(GetTriangleBlock() + offset); }
 
-		uint32						mOffsetToVertices;			///< Offset from current block to start of vertices in bytes
+		uint32						mFlags;
 	};
 
 	static_assert(sizeof(TriangleBlockHeader) == 4, "Compiler added padding");
@@ -131,15 +141,15 @@ public:
 		}
 
 		/// Get an upper bound on the amount of bytes needed to store inTriangleCount triangles
-		uint						GetPessimisticMemoryEstimate(uint inTriangleCount) const
+		uint						GetPessimisticMemoryEstimate(uint inTriangleCount, bool inStoreUserData) const
 		{
 			// Worst case each triangle is alone in a block, none of the vertices are shared and we need to add 3 bytes to align the vertices
-			return inTriangleCount * (sizeof(TriangleBlockHeader) + sizeof(TriangleBlock) + 3 * sizeof(VertexData)) + 3;
+			return inTriangleCount * (sizeof(TriangleBlockHeader) + sizeof(TriangleBlock) + (inStoreUserData? sizeof(uint32) : 0) + 3 * sizeof(VertexData)) + 3;
 		}
 
 		/// Pack the triangles in inContainer to ioBuffer. This stores the mMaterialIndex of a triangle in the 8 bit flags.
 		/// Returns uint(-1) on error.
-		uint						Pack(const IndexedTriangleList &inTriangles, ByteBuffer &ioBuffer, const char *&outError)
+		uint						Pack(const IndexedTriangleList &inTriangles, bool inStoreUserData, ByteBuffer &ioBuffer, const char *&outError)
 		{
 			// Determine position of triangles start
 			uint offset = (uint)ioBuffer.size();
@@ -155,11 +165,20 @@ public:
 			uint start_vertex = Clamp((int)mVertices.size() - 256 + (int)tri_count * 3, 0, (int)mVertices.size());
 
 			// Store the start vertex offset, this will later be patched to give the delta offset relative to the triangle block
-			mOffsetsToPatch.push_back(uint((uint8 *)&header->mOffsetToVertices - &ioBuffer[0]));
-			header->mOffsetToVertices = start_vertex * sizeof(VertexData);
+			mOffsetsToPatch.push_back(uint((uint8 *)&header->mFlags - &ioBuffer[0]));
+			header->mFlags = start_vertex * sizeof(VertexData);
+			JPH_ASSERT(header->mFlags <= OFFSET_TO_VERTICES_MASK, "Offset to vertices doesn't fit");
 
-			// Pack vertices
+			// When we store user data we need to store the offset to the user data in TriangleBlocks
 			uint padded_triangle_count = AlignUp(tri_count, 4);
+			if (inStoreUserData)
+			{
+				uint32 num_blocks = padded_triangle_count >> 2;
+				JPH_ASSERT(num_blocks <= OFFSET_TO_USERDATA_MASK);
+				header->mFlags |= num_blocks << OFFSET_TO_VERTICES_BITS;
+			}
+
+			// Pack vertices
 			for (uint t = 0; t < padded_triangle_count; t += 4)
 			{
 				TriangleBlock *block = ioBuffer.Allocate<TriangleBlock>();
@@ -199,6 +218,14 @@ public:
 					}
 			}
 
+			// Store user data
+			if (inStoreUserData)
+			{
+				uint32 *user_data = ioBuffer.Allocate<uint32>(tri_count);
+				for (uint t = 0; t < tri_count; ++t)
+					user_data[t] = inTriangles[t].mUserData;
+			}
+
 			return offset;
 		}
 
@@ -214,7 +241,13 @@ public:
 
 			// Patch the offsets
 			for (uint o : mOffsetsToPatch)
-				*ioBuffer.Get<uint32>(o) += vertices_idx - o;
+			{
+				uint32 *flags = ioBuffer.Get<uint32>(o);
+				uint32 delta = vertices_idx - o;
+				if ((*flags & OFFSET_TO_VERTICES_MASK) + delta > OFFSET_TO_VERTICES_MASK)
+					JPH_ASSERT(false, "Offset to vertices doesn't fit");
+				*flags += delta;
+			}
 
 			// Calculate bounding box
 			AABox bounds;
@@ -409,6 +442,14 @@ public:
 			outV3 = trans.GetAxisZ();
 		}
 
+		/// Get user data for a triangle
+		JPH_INLINE uint32			GetUserData(const void *inTriangleStart, uint32 inTriangleIdx) const
+		{
+			const TriangleBlockHeader *header = reinterpret_cast<const TriangleBlockHeader *>(inTriangleStart);
+			const uint32 *user_data = header->GetUserData();
+			return user_data != nullptr? user_data[inTriangleIdx] : 0;
+		}
+
 		/// Get flags for entire triangle block
 		JPH_INLINE static void		sGetFlags(const void *inTriangleStart, uint32 inNumTriangles, uint8 *outTriangleFlags)
 		{

+ 8 - 7
Jolt/Geometry/IndexedTriangle.h

@@ -75,12 +75,12 @@ public:
 	using IndexedTriangleNoMaterial::IndexedTriangleNoMaterial;
 
 	/// Constructor
-	constexpr		IndexedTriangle(uint32 inI1, uint32 inI2, uint32 inI3, uint32 inMaterialIndex) : IndexedTriangleNoMaterial(inI1, inI2, inI3), mMaterialIndex(inMaterialIndex) { }
+	constexpr		IndexedTriangle(uint32 inI1, uint32 inI2, uint32 inI3, uint32 inMaterialIndex, uint inUserData) : IndexedTriangleNoMaterial(inI1, inI2, inI3), mMaterialIndex(inMaterialIndex), mUserData(inUserData) { }
 
 	/// Check if two triangles are identical
 	bool			operator == (const IndexedTriangle &inRHS) const
 	{
-		return mMaterialIndex == inRHS.mMaterialIndex && IndexedTriangleNoMaterial::operator==(inRHS);
+		return mMaterialIndex == inRHS.mMaterialIndex && mUserData == inRHS.mUserData && IndexedTriangleNoMaterial::operator==(inRHS);
 	}
 
 	/// Rotate the vertices so that the lowest vertex becomes the first. This does not change the represented triangle.
@@ -89,20 +89,21 @@ public:
 		if (mIdx[0] < mIdx[1])
 		{
 			if (mIdx[0] < mIdx[2])
-				return IndexedTriangle(mIdx[0], mIdx[1], mIdx[2], mMaterialIndex); // 0 is smallest
+				return IndexedTriangle(mIdx[0], mIdx[1], mIdx[2], mMaterialIndex, mUserData); // 0 is smallest
 			else
-				return IndexedTriangle(mIdx[2], mIdx[0], mIdx[1], mMaterialIndex); // 2 is smallest
+				return IndexedTriangle(mIdx[2], mIdx[0], mIdx[1], mMaterialIndex, mUserData); // 2 is smallest
 		}
 		else
 		{
 			if (mIdx[1] < mIdx[2])
-				return IndexedTriangle(mIdx[1], mIdx[2], mIdx[0], mMaterialIndex); // 1 is smallest
+				return IndexedTriangle(mIdx[1], mIdx[2], mIdx[0], mMaterialIndex, mUserData); // 1 is smallest
 			else
-				return IndexedTriangle(mIdx[2], mIdx[0], mIdx[1], mMaterialIndex); // 2 is smallest
+				return IndexedTriangle(mIdx[2], mIdx[0], mIdx[1], mMaterialIndex, mUserData); // 2 is smallest
 		}
 	}
 
 	uint32			mMaterialIndex = 0;
+	uint32			mUserData = 0;				///< User data that can be used for anything by the application, e.g. for tracking the original index of the triangle
 };
 
 using IndexedTriangleNoMaterialList = Array<IndexedTriangleNoMaterial>;
@@ -112,4 +113,4 @@ JPH_NAMESPACE_END
 
 // Create a std::hash for IndexedTriangleNoMaterial and IndexedTriangle
 JPH_MAKE_HASHABLE(JPH::IndexedTriangleNoMaterial, t.mIdx[0], t.mIdx[1], t.mIdx[2])
-JPH_MAKE_HASHABLE(JPH::IndexedTriangle, t.mIdx[0], t.mIdx[1], t.mIdx[2], t.mMaterialIndex)
+JPH_MAKE_HASHABLE(JPH::IndexedTriangle, t.mIdx[0], t.mIdx[1], t.mIdx[2], t.mMaterialIndex, t.mUserData)

+ 6 - 2
Jolt/Geometry/Indexify.cpp

@@ -197,6 +197,7 @@ void Indexify(const TriangleList &inTriangles, VertexList &outVertices, IndexedT
 	{
 		IndexedTriangle it;
 		it.mMaterialIndex = inTriangles[t].mMaterialIndex;
+		it.mUserData = inTriangles[t].mUserData;
 		for (int v = 0; v < 3; ++v)
 			it.mIdx[v] = welded_vertices[t * 3 + v];
 		if (!it.IsDegenerate(outVertices))
@@ -209,9 +210,12 @@ void Deindexify(const VertexList &inVertices, const IndexedTriangleList &inTrian
 	outTriangles.resize(inTriangles.size());
 	for (size_t t = 0; t < inTriangles.size(); ++t)
 	{
-		outTriangles[t].mMaterialIndex = inTriangles[t].mMaterialIndex;
+		const IndexedTriangle &in = inTriangles[t];
+		Triangle &out = outTriangles[t];
+		out.mMaterialIndex = in.mMaterialIndex;
+		out.mUserData = in.mUserData;
 		for (int v = 0; v < 3; ++v)
-			outTriangles[t].mV[v] = inVertices[inTriangles[t].mIdx[v]];
+			out.mV[v] = inVertices[in.mIdx[v]];
 	}
 }
 

+ 3 - 3
Jolt/Geometry/Triangle.h

@@ -14,9 +14,8 @@ public:
 
 	/// Constructor
 					Triangle() = default;
-					Triangle(const Float3 &inV1, const Float3 &inV2, const Float3 &inV3) : mV { inV1, inV2, inV3 } { }
-					Triangle(const Float3 &inV1, const Float3 &inV2, const Float3 &inV3, uint32 inMaterialIndex) : Triangle(inV1, inV2, inV3) { mMaterialIndex = inMaterialIndex; }
-					Triangle(Vec3Arg inV1, Vec3Arg inV2, Vec3Arg inV3) { inV1.StoreFloat3(&mV[0]); inV2.StoreFloat3(&mV[1]); inV3.StoreFloat3(&mV[2]); }
+					Triangle(const Float3 &inV1, const Float3 &inV2, const Float3 &inV3, uint32 inMaterialIndex = 0, uint32 inUserData = 0) : mV { inV1, inV2, inV3 }, mMaterialIndex(inMaterialIndex), mUserData(inUserData) { }
+					Triangle(Vec3Arg inV1, Vec3Arg inV2, Vec3Arg inV3, uint32 inMaterialIndex = 0, uint32 inUserData = 0) : mMaterialIndex(inMaterialIndex), mUserData(inUserData) { inV1.StoreFloat3(&mV[0]); inV2.StoreFloat3(&mV[1]); inV3.StoreFloat3(&mV[2]); }
 
 	/// Get center of triangle
 	Vec3			GetCentroid() const
@@ -27,6 +26,7 @@ public:
 	/// Vertices
 	Float3			mV[3];
 	uint32			mMaterialIndex = 0;			///< Follows mV[3] so that we can read mV as 4 vectors
+	uint32			mUserData = 0;				///< User data that can be used for anything by the application, e.g. for tracking the original index of the triangle
 };
 
 using TriangleList = Array<Triangle>;

+ 2 - 0
Jolt/ObjectStream/TypeDeclarations.cpp

@@ -44,12 +44,14 @@ JPH_IMPLEMENT_SERIALIZABLE_OUTSIDE_CLASS(Triangle)
 {
 	JPH_ADD_ATTRIBUTE(Triangle, mV)
 	JPH_ADD_ATTRIBUTE(Triangle, mMaterialIndex)
+	JPH_ADD_ATTRIBUTE(Triangle, mUserData)
 }
 
 JPH_IMPLEMENT_SERIALIZABLE_OUTSIDE_CLASS(IndexedTriangle)
 {
 	JPH_ADD_ATTRIBUTE(IndexedTriangle, mIdx)
 	JPH_ADD_ATTRIBUTE(IndexedTriangle, mMaterialIndex)
+	JPH_ADD_ATTRIBUTE(IndexedTriangle, mUserData)
 }
 
 JPH_IMPLEMENT_SERIALIZABLE_OUTSIDE_CLASS(Plane)

+ 16 - 0
Jolt/Physics/Collision/Shape/CompoundShape.cpp

@@ -128,6 +128,22 @@ const PhysicsMaterial *CompoundShape::GetMaterial(const SubShapeID &inSubShapeID
 	return mSubShapes[index].mShape->GetMaterial(remainder);
 }
 
+const Shape *CompoundShape::GetLeafShape(const SubShapeID &inSubShapeID, SubShapeID &outRemainder) const
+{
+	// Decode sub shape index
+	SubShapeID remainder;
+	uint32 index = GetSubShapeIndexFromID(inSubShapeID, remainder);
+	if (index >= mSubShapes.size())
+	{
+		// No longer valid index
+		outRemainder = SubShapeID();
+		return nullptr;
+	}
+
+	// Pass call on
+	return mSubShapes[index].mShape->GetLeafShape(remainder, outRemainder);
+}
+
 uint64 CompoundShape::GetSubShapeUserData(const SubShapeID &inSubShapeID) const
 {
 	// Decode sub shape index

+ 3 - 0
Jolt/Physics/Collision/Shape/CompoundShape.h

@@ -79,6 +79,9 @@ public:
 	// See Shape::GetMaterial
 	virtual const PhysicsMaterial *	GetMaterial(const SubShapeID &inSubShapeID) const override;
 
+	// See Shape::GetLeafShape
+	virtual const Shape *			GetLeafShape(const SubShapeID &inSubShapeID, SubShapeID &outRemainder) const override;
+
 	// See Shape::GetSubShapeUserData
 	virtual uint64					GetSubShapeUserData(const SubShapeID &inSubShapeID) const override;
 

+ 3 - 0
Jolt/Physics/Collision/Shape/DecoratedShape.h

@@ -47,6 +47,9 @@ public:
 	// See Shape::GetSubShapeIDBitsRecursive
 	virtual uint					GetSubShapeIDBitsRecursive() const override				{ return mInnerShape->GetSubShapeIDBitsRecursive(); }
 
+	// See Shape::GetLeafShape
+	virtual const Shape *			GetLeafShape(const SubShapeID &inSubShapeID, SubShapeID &outRemainder) const override { return mInnerShape->GetLeafShape(inSubShapeID, outRemainder); }
+
 	// See Shape::GetMaterial
 	virtual const PhysicsMaterial *	GetMaterial(const SubShapeID &inSubShapeID) const override;
 

+ 14 - 1
Jolt/Physics/Collision/Shape/MeshShape.cpp

@@ -54,6 +54,7 @@ JPH_IMPLEMENT_SERIALIZABLE_VIRTUAL(MeshShapeSettings)
 	JPH_ADD_ATTRIBUTE(MeshShapeSettings, mMaterials)
 	JPH_ADD_ATTRIBUTE(MeshShapeSettings, mMaxTrianglesPerLeaf)
 	JPH_ADD_ATTRIBUTE(MeshShapeSettings, mActiveEdgeCosThresholdAngle)
+	JPH_ADD_ATTRIBUTE(MeshShapeSettings, mPerTriangleUserData)
 }
 
 // Codecs this mesh shape is using
@@ -199,7 +200,7 @@ MeshShape::MeshShape(const MeshShapeSettings &inSettings, ShapeResult &outResult
 	// Convert to buffer
 	AABBTreeToBuffer<TriangleCodec, NodeCodec> buffer;
 	const char *error = nullptr;
-	if (!buffer.Convert(inSettings.mTriangleVertices, root, error))
+	if (!buffer.Convert(inSettings.mTriangleVertices, root, inSettings.mPerTriangleUserData, error))
 	{
 		outResult.SetError(error);
 		delete root;
@@ -1221,6 +1222,18 @@ Shape::Stats MeshShape::GetStats() const
 	return Stats(sizeof(*this) + mMaterials.size() * sizeof(Ref<PhysicsMaterial>) + mTree.size() * sizeof(uint8), visitor.mNumTriangles);
 }
 
+uint32 MeshShape::GetTriangleUserData(const SubShapeID &inSubShapeID) const
+{
+	// Decode ID
+	const void *block_start;
+	uint32 triangle_idx;
+	DecodeSubShapeID(inSubShapeID, block_start, triangle_idx);
+
+	// Decode triangle
+	const TriangleCodec::DecodingContext triangle_ctx(sGetTriangleHeader(mTree));
+	return triangle_ctx.GetUserData(block_start, triangle_idx);
+}
+
 void MeshShape::sRegister()
 {
 	ShapeFunctions &f = ShapeFunctions::sGet(EShapeSubType::Mesh);

+ 9 - 0
Jolt/Physics/Collision/Shape/MeshShape.h

@@ -59,6 +59,12 @@ public:
 	/// Setting this value too small can cause ghost collisions with edges, setting it too big can cause depenetration artifacts (objects not depenetrating quickly).
 	/// Valid ranges are between cos(0 degrees) and cos(90 degrees). The default value is cos(5 degrees).
 	float							mActiveEdgeCosThresholdAngle = 0.996195f;					// cos(5 degrees)
+
+	/// When true, we store the user data coming from Triangle::mUserData or IndexedTriangle::mUserData in the mesh shape.
+	/// This can be used to store additional data like the original index of the triangle in the mesh.
+	/// Can be retrieved using MeshShape::GetTriangleUserData.
+	/// Turning this on increases the memory used by the MeshShape by roughly 25%.
+	bool							mPerTriangleUserData = false;
 };
 
 /// A mesh shape, consisting of triangles. Mesh shapes are mostly used for static geometry.
@@ -141,6 +147,9 @@ public:
 	// See Shape::GetVolume
 	virtual float					GetVolume() const override									{ return 0; }
 
+	// When MeshShape::mPerTriangleUserData is true, this function can be used to retrieve the user data that was stored in the mesh shape.
+	uint32							GetTriangleUserData(const SubShapeID &inSubShapeID) const;
+
 #ifdef JPH_DEBUG_RENDERER
 	// Settings
 	static bool						sDrawTriangleGroups;

+ 6 - 0
Jolt/Physics/Collision/Shape/Shape.cpp

@@ -32,6 +32,12 @@ bool Shape::sDrawSubmergedVolumes = false;
 
 ShapeFunctions ShapeFunctions::sRegistry[NumSubShapeTypes];
 
+const Shape *Shape::GetLeafShape([[maybe_unused]] const SubShapeID &inSubShapeID, SubShapeID &outRemainder) const
+{
+	outRemainder = inSubShapeID;
+	return this;
+}
+
 TransformedShape Shape::GetSubShapeTransformedShape(const SubShapeID &inSubShapeID, Vec3Arg inPositionCOM, QuatArg inRotation, Vec3Arg inScale, SubShapeID &outRemainder) const
 {
 	// We have reached the leaf shape so there is no remainder

+ 7 - 1
Jolt/Physics/Collision/Shape/Shape.h

@@ -237,6 +237,12 @@ public:
 	/// Calculate the mass and inertia of this shape
 	virtual MassProperties			GetMassProperties() const = 0;
 
+	/// Get the leaf shape for a particular sub shape ID.
+	/// @param inSubShapeID The full sub shape ID that indicates the path to the leaf shape
+	/// @param outRemainder What remains of the sub shape ID after removing the path to the leaf shape (could e.g. refer to a triangle within a MeshShape)
+	/// @return The shape or null if the sub shape ID is invalid
+	virtual const Shape *			GetLeafShape([[maybe_unused]] const SubShapeID &inSubShapeID, SubShapeID &outRemainder) const;
+
 	/// Get the material assigned to a particular sub shape ID
 	virtual const PhysicsMaterial *	GetMaterial(const SubShapeID &inSubShapeID) const = 0;
 
@@ -256,7 +262,7 @@ public:
 	/// @param outVertices Resulting face. The returned face can be empty if the shape doesn't have polygons to return (e.g. because it's a sphere). The face will be returned in world space.
 	virtual void					GetSupportingFace([[maybe_unused]] const SubShapeID &inSubShapeID, [[maybe_unused]] Vec3Arg inDirection, [[maybe_unused]] Vec3Arg inScale, [[maybe_unused]] Mat44Arg inCenterOfMassTransform, [[maybe_unused]] SupportingFace &outVertices) const { /* Nothing */ }
 
-	/// Get the user data of a particular sub shape ID
+	/// Get the user data of a particular sub shape ID. Corresponds with the value stored in Shape::GetUserData of the leaf shape pointed to by inSubShapeID.
 	virtual uint64					GetSubShapeUserData([[maybe_unused]] const SubShapeID &inSubShapeID) const			{ return mUserData; }
 
 	/// Get the direct child sub shape and its transform for a sub shape ID.

+ 2 - 0
Samples/Samples.cmake

@@ -237,6 +237,8 @@ set(SAMPLES_SRC_FILES
 	${SAMPLES_ROOT}/Tests/Shapes/HeightFieldShapeTest.h
 	${SAMPLES_ROOT}/Tests/Shapes/MeshShapeTest.cpp
 	${SAMPLES_ROOT}/Tests/Shapes/MeshShapeTest.h
+	${SAMPLES_ROOT}/Tests/Shapes/MeshShapeUserDataTest.cpp
+	${SAMPLES_ROOT}/Tests/Shapes/MeshShapeUserDataTest.h
 	${SAMPLES_ROOT}/Tests/Shapes/SphereShapeTest.cpp
 	${SAMPLES_ROOT}/Tests/Shapes/SphereShapeTest.h
 	${SAMPLES_ROOT}/Tests/Shapes/PlaneShapeTest.cpp

+ 2 - 0
Samples/SamplesApp.cpp

@@ -208,6 +208,7 @@ JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, TriangleShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, PlaneShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, ConvexHullShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, MeshShapeTest)
+JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, MeshShapeUserDataTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, HeightFieldShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, DeformedHeightFieldShapeTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, RotatedTranslatedShapeTest)
@@ -222,6 +223,7 @@ static TestNameAndRTTI sShapeTests[] =
 	{ "Cylinder Shape",						JPH_RTTI(CylinderShapeTest) },
 	{ "Convex Hull Shape",					JPH_RTTI(ConvexHullShapeTest) },
 	{ "Mesh Shape",							JPH_RTTI(MeshShapeTest) },
+	{ "Mesh Shape Per Triangle User Data",	JPH_RTTI(MeshShapeUserDataTest) },
 	{ "Height Field Shape",					JPH_RTTI(HeightFieldShapeTest) },
 	{ "Deformed Height Field Shape",		JPH_RTTI(DeformedHeightFieldShapeTest) },
 	{ "Static Compound Shape",				JPH_RTTI(StaticCompoundShapeTest) },

+ 94 - 0
Samples/Tests/Shapes/MeshShapeUserDataTest.cpp

@@ -0,0 +1,94 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Tests/Shapes/MeshShapeUserDataTest.h>
+#include <Jolt/Physics/Collision/Shape/BoxShape.h>
+#include <Jolt/Physics/Collision/Shape/MeshShape.h>
+#include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
+#include <Jolt/Physics/Collision/RayCast.h>
+#include <Jolt/Physics/Collision/CastResult.h>
+#include <Jolt/Geometry/Triangle.h>
+#include <Jolt/Physics/Body/BodyCreationSettings.h>
+#include <Layers.h>
+#include <Renderer/DebugRendererImp.h>
+
+JPH_IMPLEMENT_RTTI_VIRTUAL(MeshShapeUserDataTest)
+{
+	JPH_ADD_BASE_CLASS(MeshShapeUserDataTest, Test)
+}
+
+void MeshShapeUserDataTest::Initialize()
+{
+	std::default_random_engine random;
+
+	// Create regular grid of triangles
+	uint32 user_data = 0;
+	TriangleList triangles[2];
+	for (int x = -10; x < 10; ++x)
+		for (int z = -10; z < 10; ++z)
+		{
+			float x1 = 10.0f * x;
+			float z1 = 10.0f * z;
+			float x2 = x1 + 10.0f;
+			float z2 = z1 + 10.0f;
+
+			Float3 v1 = Float3(x1, 0, z1);
+			Float3 v2 = Float3(x2, 0, z1);
+			Float3 v3 = Float3(x1, 0, z2);
+			Float3 v4 = Float3(x2, 0, z2);
+
+			triangles[random() & 1].push_back(Triangle(v1, v3, v4, 0, user_data++));
+			triangles[random() & 1].push_back(Triangle(v1, v4, v2, 0, user_data++));
+		}
+
+	// Create a compound with 2 meshes
+	StaticCompoundShapeSettings compound_settings;
+	compound_settings.SetEmbedded();
+	for (TriangleList &t : triangles)
+	{
+		// Shuffle the triangles
+		std::shuffle(t.begin(), t.end(), random);
+
+		// Create mesh
+		MeshShapeSettings mesh_settings(t);
+		mesh_settings.mPerTriangleUserData = true;
+		compound_settings.AddShape(Vec3::sZero(), Quat::sIdentity(), mesh_settings.Create().Get());
+	}
+
+	// Create body
+	mBodyInterface->CreateAndAddBody(BodyCreationSettings(&compound_settings, RVec3::sZero(), Quat::sRotation(Vec3::sAxisX(), 0.25f * JPH_PI), EMotionType::Static, Layers::NON_MOVING), EActivation::DontActivate);
+
+	// 1 body with zero friction
+	BodyCreationSettings bcs(new BoxShape(Vec3::sReplicate(2.0f)), RVec3(0, 55.0f, -50.0f), Quat::sRotation(Vec3::sAxisX(), 0.25f * JPH_PI), EMotionType::Dynamic, Layers::MOVING);
+	bcs.mFriction = 0.0f;
+	bcs.mEnhancedInternalEdgeRemoval = true; // Needed because the 2 meshes have a lot of active edges
+	mBodyInterface->CreateAndAddBody(bcs, EActivation::Activate);
+}
+
+void MeshShapeUserDataTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+{
+	// Cast a ray
+	RayCastResult hit;
+	RRayCast ray(inParams.mCameraState.mPos, inParams.mCameraState.mForward * 100.0f);
+	mPhysicsSystem->GetNarrowPhaseQuery().CastRay(ray, hit);
+
+	// Get body (if there was a hit)
+	BodyLockRead lock(mPhysicsSystem->GetBodyLockInterface(), hit.mBodyID);
+	if (lock.SucceededAndIsInBroadPhase())
+	{
+		// Get the leaf shape (mesh shape in this case)
+		SubShapeID remainder;
+		const Shape *shape = lock.GetBody().GetShape()->GetLeafShape(hit.mSubShapeID2, remainder);
+		JPH_ASSERT(shape->GetType() == EShapeType::Mesh);
+
+		// Get user data from the triangle that was hit
+		uint32 user_data = static_cast<const MeshShape *>(shape)->GetTriangleUserData(remainder);
+
+		// Draw it on screen
+		RVec3 hit_pos = ray.GetPointOnRay(hit.mFraction);
+		mDebugRenderer->DrawText3D(hit_pos, StringFormat("UserData: %d", user_data).c_str());
+	}
+}

+ 18 - 0
Samples/Tests/Shapes/MeshShapeUserDataTest.h

@@ -0,0 +1,18 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Tests/Test.h>
+
+// Shows how to store per triangle user data in a mesh shape and how to retrieve it
+class MeshShapeUserDataTest : public Test
+{
+public:
+	JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, MeshShapeUserDataTest)
+
+	// See: Test
+	virtual void	Initialize() override;
+	virtual void	PrePhysicsUpdate(const PreUpdateParams &inParams) override;
+};

+ 74 - 0
UnitTests/Physics/ShapeTests.cpp

@@ -21,6 +21,7 @@
 #include <Jolt/Physics/Collision/CollidePointResult.h>
 #include <Jolt/Physics/Collision/RayCast.h>
 #include <Jolt/Physics/Collision/CastResult.h>
+#include <Jolt/Physics/Collision/CollisionDispatch.h>
 #include <Jolt/Core/StreamWrapper.h>
 
 TEST_SUITE("ShapeTests")
@@ -809,6 +810,79 @@ TEST_SUITE("ShapeTests")
 		}
 	}
 
+	TEST_CASE("TestMeshShapePerTriangleUserData")
+	{
+		UnitTestRandom random;
+
+		// Create regular grid of triangles
+		TriangleList triangles[2];
+		for (int x = 0; x < 20; ++x)
+			for (int z = 0; z < 20; ++z)
+			{
+				float x1 = 10.0f * x;
+				float z1 = 10.0f * z;
+				float x2 = x1 + 10.0f;
+				float z2 = z1 + 10.0f;
+
+				Float3 v1 = Float3(x1, 0, z1);
+				Float3 v2 = Float3(x2, 0, z1);
+				Float3 v3 = Float3(x1, 0, z2);
+				Float3 v4 = Float3(x2, 0, z2);
+
+				uint32 user_data = (x << 16) + z;
+				triangles[random() & 1].push_back(Triangle(v1, v3, v4, 0, user_data));
+				triangles[random() & 1].push_back(Triangle(v1, v4, v2, 0, user_data | 0x80000000));
+			}
+
+		// Create a compound with 2 meshes
+		StaticCompoundShapeSettings compound_settings;
+		compound_settings.SetEmbedded();
+		for (TriangleList &t : triangles)
+		{
+			// Shuffle the triangles
+			std::shuffle(t.begin(), t.end(), random);
+
+			// Create mesh
+			MeshShapeSettings mesh_settings(t);
+			mesh_settings.mPerTriangleUserData = true;
+			compound_settings.AddShape(Vec3::sZero(), Quat::sIdentity(), mesh_settings.Create().Get());
+		}
+		RefConst<Shape> compound = compound_settings.Create().Get();
+
+		// Collide the compound with a box to get all triangles back
+		RefConst<Shape> box = new BoxShape(Vec3::sReplicate(100.0f));
+		AllHitCollisionCollector<CollideShapeCollector> collector;
+		CollideShapeSettings settings;
+		settings.mCollectFacesMode = ECollectFacesMode::CollectFaces;
+		CollisionDispatch::sCollideShapeVsShape(box, compound, Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), Mat44::sTranslation(Vec3(100.0f, 0, 100.0f)), Mat44::sIdentity(), SubShapeIDCreator(), SubShapeIDCreator(), settings, collector);
+		CHECK(collector.mHits.size() == triangles[0].size() + triangles[1].size());
+		for (const CollideShapeResult &r : collector.mHits)
+		{
+			// Get average vertex
+			Vec3 avg = Vec3::sZero();
+			for (const Vec3 &v : r.mShape2Face)
+				avg += v;
+
+			// Calculate the expected user data
+			avg = avg / 30.0f;
+			uint x = uint(avg.GetX());
+			uint z = uint(avg.GetZ());
+			uint32 expected_user_data = (x << 16) + z;
+			if (avg.GetX() - float(x) > 0.5f)
+				expected_user_data |= 0x80000000;
+
+			// Get the leaf shape (mesh shape in this case)
+			SubShapeID remainder;
+			const Shape *shape = compound->GetLeafShape(r.mSubShapeID2, remainder);
+			JPH_ASSERT(shape->GetType() == EShapeType::Mesh);
+
+			// Get user data from the triangle that was hit
+			uint32 user_data = static_cast<const MeshShape *>(shape)->GetTriangleUserData(remainder);
+
+			CHECK(user_data == expected_user_data);
+		}
+	}
+
 	TEST_CASE("TestMutableCompoundShapeAdjustCenterOfMass")
 	{
 		// Start with a box at (-1 0 0)