Browse Source

Soft body skinning constraints (#947)

This can be used to limit the movement of soft body vertices based on a skinned mesh. You can specify a 'backstop' which is the max distance behind the plane formed by the skinned vertex and the averaged normal based on adjacent faces and a 'max distance' which stops the vertex when it moves more than this distance away from the vertex. This is mainly suitable for simulating clothing where you don't want to use highly detailed collision volumes to limit the movement of the soft body.
Jorrit Rouwe 1 year ago
parent
commit
277b818ffe

+ 3 - 0
Jolt/Physics/Body/BodyManager.cpp

@@ -1085,6 +1085,9 @@ void BodyManager::Draw(const DrawSettings &inDrawSettings, const PhysicsSettings
 				if (inDrawSettings.mDrawSoftBodyVolumeConstraints)
 				if (inDrawSettings.mDrawSoftBodyVolumeConstraints)
 					mp->DrawVolumeConstraints(inRenderer, com);
 					mp->DrawVolumeConstraints(inRenderer, com);
 
 
+				if (inDrawSettings.mDrawSoftBodySkinConstraints)
+					mp->DrawSkinConstraints(inRenderer);
+
 				if (inDrawSettings.mDrawSoftBodyPredictedBounds)
 				if (inDrawSettings.mDrawSoftBodyPredictedBounds)
 					mp->DrawPredictedBounds(inRenderer, com);
 					mp->DrawPredictedBounds(inRenderer, com);
 			}
 			}

+ 1 - 0
Jolt/Physics/Body/BodyManager.h

@@ -232,6 +232,7 @@ public:
 		bool						mDrawSoftBodyVertices = false;					///< Draw the vertices of soft bodies
 		bool						mDrawSoftBodyVertices = false;					///< Draw the vertices of soft bodies
 		bool						mDrawSoftBodyEdgeConstraints = false;			///< Draw the edge constraints of soft bodies
 		bool						mDrawSoftBodyEdgeConstraints = false;			///< Draw the edge constraints of soft bodies
 		bool						mDrawSoftBodyVolumeConstraints = false;			///< Draw the volume constraints of soft bodies
 		bool						mDrawSoftBodyVolumeConstraints = false;			///< Draw the volume constraints of soft bodies
+		bool						mDrawSoftBodySkinConstraints = false;			///< Draw the skin constraints of soft bodies
 		bool						mDrawSoftBodyPredictedBounds = false;			///< Draw the predicted bounds of soft bodies
 		bool						mDrawSoftBodyPredictedBounds = false;			///< Draw the predicted bounds of soft bodies
 	};
 	};
 
 

+ 129 - 0
Jolt/Physics/SoftBody/SoftBodyMotionProperties.cpp

@@ -74,6 +74,10 @@ void SoftBodyMotionProperties::Initialize(const SoftBodyCreationSettings &inSett
 		mLocalBounds.Encapsulate(out_vertex.mPosition);
 		mLocalBounds.Encapsulate(out_vertex.mPosition);
 	}
 	}
 
 
+	// Allocate space for skinned vertices
+	if (!inSettings.mSettings->mSkinnedConstraints.empty())
+		mSkinState.resize(mVertices.size());
+
 	// We don't know delta time yet, so we can't predict the bounds and use the local bounds as the predicted bounds
 	// We don't know delta time yet, so we can't predict the bounds and use the local bounds as the predicted bounds
 	mLocalPredictedBounds = mLocalBounds;
 	mLocalPredictedBounds = mLocalBounds;
 
 
@@ -288,6 +292,50 @@ void SoftBodyMotionProperties::ApplyVolumeConstraints(const SoftBodyUpdateContex
 	}
 	}
 }
 }
 
 
+void SoftBodyMotionProperties::ApplySkinConstraints([[maybe_unused]] const SoftBodyUpdateContext &inContext)
+{
+	// Early out if nothing to do
+	if (mSettings->mSkinnedConstraints.empty())
+		return;
+
+	JPH_ASSERT(mSkinStateTransform == inContext.mCenterOfMassTransform, "Skinning state is stale, artifacts will show!");
+
+	// Apply the constraints
+	Vertex *vertices = mVertices.data();
+	const SkinState *skin_states = mSkinState.data();
+	for (const Skinned &s : mSettings->mSkinnedConstraints)
+	{
+		Vertex &vertex = vertices[s.mVertex];
+		const SkinState &skin_state = skin_states[s.mVertex];
+		if (vertex.mInvMass > 0.0f)
+		{
+			// Clamp vertex distance to max distance from skinned position
+			if (s.mMaxDistance < FLT_MAX)
+			{
+				Vec3 delta = vertex.mPosition - skin_state.mPosition;
+				float delta_len_sq = delta.LengthSq();
+				float max_distance_sq = Square(s.mMaxDistance);
+				if (delta_len_sq > max_distance_sq)
+					vertex.mPosition = skin_state.mPosition + delta * sqrt(max_distance_sq / delta_len_sq);
+			}
+
+			// Move position if it violated the back stop
+			if (s.mBackStop < s.mMaxDistance)
+			{
+				Vec3 delta = vertex.mPosition - skin_state.mPosition;
+				float violation = -s.mBackStop - skin_state.mNormal.Dot(delta);
+				if (violation > 0.0f)
+					vertex.mPosition += violation * skin_state.mNormal;
+			}
+		}
+		else
+		{
+			// Kinematic: Just update the vertex position
+			vertex.mPosition = skin_state.mPosition;
+		}
+	}
+}
+
 void SoftBodyMotionProperties::ApplyEdgeConstraints(const SoftBodyUpdateContext &inContext, uint inStartIndex, uint inEndIndex)
 void SoftBodyMotionProperties::ApplyEdgeConstraints(const SoftBodyUpdateContext &inContext, uint inStartIndex, uint inEndIndex)
 {
 {
 	JPH_PROFILE_FUNCTION();
 	JPH_PROFILE_FUNCTION();
@@ -624,6 +672,8 @@ SoftBodyMotionProperties::EStatus SoftBodyMotionProperties::ParallelApplyEdgeCon
 							// Finish the iteration
 							// Finish the iteration
 							ApplyCollisionConstraintsAndUpdateVelocities(ioContext);
 							ApplyCollisionConstraintsAndUpdateVelocities(ioContext);
 
 
+							ApplySkinConstraints(ioContext);
+
 							uint iteration = ioContext.mNextIteration.fetch_add(1, memory_order_relaxed);
 							uint iteration = ioContext.mNextIteration.fetch_add(1, memory_order_relaxed);
 							if (iteration < mNumIterations)
 							if (iteration < mNumIterations)
 							{
 							{
@@ -677,6 +727,76 @@ SoftBodyMotionProperties::EStatus SoftBodyMotionProperties::ParallelUpdate(SoftB
 	}
 	}
 }
 }
 
 
+void SoftBodyMotionProperties::SkinVertices(RMat44Arg inRootTransform, const Mat44 *inJointMatrices, [[maybe_unused]] uint inNumJoints, bool inHardSkinAll, TempAllocator &ioTempAllocator)
+{
+	// Calculate the skin matrices
+	uint num_skin_matrices = uint(mSettings->mInvBindMatrices.size());
+	uint skin_matrices_size = num_skin_matrices * sizeof(Mat44);
+	Mat44 *skin_matrices = (Mat44 *)ioTempAllocator.Allocate(skin_matrices_size);
+	const Mat44 *skin_matrices_end = skin_matrices + num_skin_matrices;
+	const InvBind *inv_bind_matrix = mSettings->mInvBindMatrices.data();
+	for (Mat44 *s = skin_matrices; s < skin_matrices_end; ++s, ++inv_bind_matrix)
+		*s = inJointMatrices[inv_bind_matrix->mJointIndex] * inv_bind_matrix->mInvBind;
+
+	// Skin the vertices
+	mSkinStateTransform = inRootTransform;
+	JPH_IF_ENABLE_ASSERTS(uint num_vertices = uint(mSettings->mVertices.size());)
+	JPH_ASSERT(mSkinState.size() == num_vertices);
+	const SoftBodySharedSettings::Vertex *in_vertices = mSettings->mVertices.data();
+	for (const Skinned &s : mSettings->mSkinnedConstraints)
+	{
+		// Get bind pose
+		JPH_ASSERT(s.mVertex < num_vertices);
+		Vec3 bind_pos = Vec3::sLoadFloat3Unsafe(in_vertices[s.mVertex].mPosition);
+
+		// Skin vertex
+		Vec3 pos = Vec3::sZero();
+		for (const SkinWeight &w : s.mWeights)
+		{
+			JPH_ASSERT(w.mInvBindIndex < num_skin_matrices);
+			pos += w.mWeight * (skin_matrices[w.mInvBindIndex] * bind_pos);
+		}
+		mSkinState[s.mVertex].mPosition = pos;
+	}
+
+	// Calculate the normals
+	for (const Skinned &s : mSettings->mSkinnedConstraints)
+	{
+		Vec3 normal = Vec3::sZero();
+		uint32 num_faces = s.mNormalInfo >> 24;
+		if (num_faces > 0)
+		{
+			// Calculate normal
+			const uint32 *f = &mSettings->mSkinnedConstraintNormals[s.mNormalInfo & 0xffffff];
+			const uint32 *f_end = f + num_faces;
+			while (f < f_end)
+			{
+				const Face &face = mSettings->mFaces[*f];
+				Vec3 v0 = mSkinState[face.mVertex[0]].mPosition;
+				Vec3 v1 = mSkinState[face.mVertex[1]].mPosition;
+				Vec3 v2 = mSkinState[face.mVertex[2]].mPosition;
+				normal += (v1 - v0).Cross(v2 - v0).NormalizedOr(Vec3::sZero());
+				++f;
+			}
+			normal /= float(num_faces);
+		}
+		mSkinState[s.mVertex].mNormal = normal;
+	}
+
+	ioTempAllocator.Free(skin_matrices, skin_matrices_size);
+
+	if (inHardSkinAll)
+	{
+		// Hard skin all vertices and reset their velocities
+		for (const Skinned &s : mSettings->mSkinnedConstraints)
+		{
+			Vertex &vertex = mVertices[s.mVertex];
+			vertex.mPosition = mSkinState[s.mVertex].mPosition;
+			vertex.mVelocity = Vec3::sZero();
+		}
+	}
+}
+
 void SoftBodyMotionProperties::CustomUpdate(float inDeltaTime, Body &ioSoftBody, PhysicsSystem &inSystem)
 void SoftBodyMotionProperties::CustomUpdate(float inDeltaTime, Body &ioSoftBody, PhysicsSystem &inSystem)
 {
 {
 	JPH_PROFILE_FUNCTION();
 	JPH_PROFILE_FUNCTION();
@@ -733,6 +853,15 @@ void SoftBodyMotionProperties::DrawVolumeConstraints(DebugRenderer *inRenderer,
 	}
 	}
 }
 }
 
 
+void SoftBodyMotionProperties::DrawSkinConstraints(DebugRenderer *inRenderer) const
+{
+	for (const Skinned &s : mSettings->mSkinnedConstraints)
+	{
+		const SkinState &skin_state = mSkinState[s.mVertex];
+		DebugRenderer::sInstance->DrawArrow(mSkinStateTransform * skin_state.mPosition, mSkinStateTransform * (skin_state.mPosition + 0.1f * skin_state.mNormal), Color::sOrange, 0.01f);
+	}
+}
+
 void SoftBodyMotionProperties::DrawPredictedBounds(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const
 void SoftBodyMotionProperties::DrawPredictedBounds(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const
 {
 {
 	inRenderer->DrawWireBox(inCenterOfMassTransform, mLocalPredictedBounds, Color::sRed);
 	inRenderer->DrawWireBox(inCenterOfMassTransform, mLocalPredictedBounds, Color::sRed);

+ 25 - 0
Jolt/Physics/SoftBody/SoftBodyMotionProperties.h

@@ -20,6 +20,7 @@ struct PhysicsSettings;
 class Body;
 class Body;
 class Shape;
 class Shape;
 class SoftBodyCreationSettings;
 class SoftBodyCreationSettings;
+class TempAllocator;
 #ifdef JPH_DEBUG_RENDERER
 #ifdef JPH_DEBUG_RENDERER
 class DebugRenderer;
 class DebugRenderer;
 #endif // JPH_DEBUG_RENDERER
 #endif // JPH_DEBUG_RENDERER
@@ -35,6 +36,9 @@ public:
 	using Edge = SoftBodySharedSettings::Edge;
 	using Edge = SoftBodySharedSettings::Edge;
 	using Face = SoftBodySharedSettings::Face;
 	using Face = SoftBodySharedSettings::Face;
 	using Volume = SoftBodySharedSettings::Volume;
 	using Volume = SoftBodySharedSettings::Volume;
+	using InvBind = SoftBodySharedSettings::InvBind;
+	using SkinWeight = SoftBodySharedSettings::SkinWeight;
+	using Skinned = SoftBodySharedSettings::Skinned;
 
 
 	/// Initialize the soft body motion properties
 	/// Initialize the soft body motion properties
 	void								Initialize(const SoftBodyCreationSettings &inSettings);
 	void								Initialize(const SoftBodyCreationSettings &inSettings);
@@ -85,6 +89,7 @@ public:
 	void								DrawVertices(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const;
 	void								DrawVertices(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const;
 	void								DrawEdgeConstraints(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const;
 	void								DrawEdgeConstraints(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const;
 	void								DrawVolumeConstraints(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const;
 	void								DrawVolumeConstraints(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const;
+	void								DrawSkinConstraints(DebugRenderer *inRenderer) const;
 	void								DrawPredictedBounds(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const;
 	void								DrawPredictedBounds(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const;
 #endif // JPH_DEBUG_RENDERER
 #endif // JPH_DEBUG_RENDERER
 
 
@@ -94,6 +99,14 @@ public:
 	/// Restoring state for replay
 	/// Restoring state for replay
 	void								RestoreState(StateRecorder &inStream);
 	void								RestoreState(StateRecorder &inStream);
 
 
+	/// Skin vertices to supplied joints, information is used by the skinned constraints.
+	/// @param inRootTransform Value of Body::GetCenterOfMassTransform().
+	/// @param inJointMatrices The joint matrices must be expressed relative to inRootTransform.
+	/// @param inNumJoints Indicates how large the inJointMatrices array is (used only for validating out of bounds).
+	/// @param inHardSkinAll Can be used to position all vertices on the skinned vertices and can be used to hard reset the soft body.
+	/// @param ioTempAllocator Allocator.
+	void								SkinVertices(RMat44Arg inRootTransform, const Mat44 *inJointMatrices, uint inNumJoints, bool inHardSkinAll, TempAllocator &ioTempAllocator);
+
 	/// This function allows you to update the soft body immediately without going through the PhysicsSystem.
 	/// This function allows you to update the soft body immediately without going through the PhysicsSystem.
 	/// This is useful if the soft body is teleported and needs to 'settle' or it can be used if a the soft body
 	/// This is useful if the soft body is teleported and needs to 'settle' or it can be used if a the soft body
 	/// is not added to the PhysicsSystem and needs to be updated manually. One reason for not adding it to the
 	/// is not added to the PhysicsSystem and needs to be updated manually. One reason for not adding it to the
@@ -161,6 +174,13 @@ private:
 		Vec3							mOriginalAngularVelocity;					///< Angular velocity of the body in local space to the soft body at start
 		Vec3							mOriginalAngularVelocity;					///< Angular velocity of the body in local space to the soft body at start
 	};
 	};
 
 
+	// Information about the state of all skinned vertices
+	struct SkinState
+	{
+		Vec3							mPosition = Vec3::sNaN();
+		Vec3							mNormal = Vec3::sNaN();
+	};
+
 	/// Do a narrow phase check and determine the closest feature that we can collide with
 	/// Do a narrow phase check and determine the closest feature that we can collide with
 	void								DetermineCollisionPlanes(const SoftBodyUpdateContext &inContext, uint inVertexStart, uint inNumVertices);
 	void								DetermineCollisionPlanes(const SoftBodyUpdateContext &inContext, uint inVertexStart, uint inNumVertices);
 
 
@@ -173,6 +193,9 @@ private:
 	/// Enforce all volume constraints
 	/// Enforce all volume constraints
 	void								ApplyVolumeConstraints(const SoftBodyUpdateContext &inContext);
 	void								ApplyVolumeConstraints(const SoftBodyUpdateContext &inContext);
 
 
+	/// Enforce all skin constraints
+	void								ApplySkinConstraints(const SoftBodyUpdateContext &inContext);
+
 	/// Enforce all edge constraints
 	/// Enforce all edge constraints
 	void								ApplyEdgeConstraints(const SoftBodyUpdateContext &inContext, uint inStartIndex, uint inEndIndex);
 	void								ApplyEdgeConstraints(const SoftBodyUpdateContext &inContext, uint inStartIndex, uint inEndIndex);
 
 
@@ -194,9 +217,11 @@ private:
 	/// Returns 6 times the volume of the soft body
 	/// Returns 6 times the volume of the soft body
 	float								GetVolumeTimesSix() const;
 	float								GetVolumeTimesSix() const;
 
 
+	RMat44								mSkinStateTransform = RMat44::sIdentity();	///< The matrix that transforms mSkinState to world space
 	RefConst<SoftBodySharedSettings>	mSettings;									///< Configuration of the particles and constraints
 	RefConst<SoftBodySharedSettings>	mSettings;									///< Configuration of the particles and constraints
 	Array<Vertex>						mVertices;									///< Current state of all vertices in the simulation
 	Array<Vertex>						mVertices;									///< Current state of all vertices in the simulation
 	Array<CollidingShape>				mCollidingShapes;							///< List of colliding shapes retrieved during the last update
 	Array<CollidingShape>				mCollidingShapes;							///< List of colliding shapes retrieved during the last update
+	Array<SkinState>					mSkinState;									///< List of skinned positions (1-on-1 with mVertices but only those that are used by the skinning constraints are filled in)
 	AABox								mLocalBounds;								///< Bounding box of all vertices
 	AABox								mLocalBounds;								///< Bounding box of all vertices
 	AABox								mLocalPredictedBounds;						///< Predicted bounding box for all vertices using extrapolation of velocity by last step delta time
 	AABox								mLocalPredictedBounds;						///< Predicted bounding box for all vertices using extrapolation of velocity by last step delta time
 	uint32								mNumIterations;								///< Number of solver iterations
 	uint32								mNumIterations;								///< Number of solver iterations

+ 93 - 0
Jolt/Physics/SoftBody/SoftBodySharedSettings.cpp

@@ -10,6 +10,8 @@
 #include <Jolt/Core/StreamIn.h>
 #include <Jolt/Core/StreamIn.h>
 #include <Jolt/Core/StreamOut.h>
 #include <Jolt/Core/StreamOut.h>
 #include <Jolt/Core/QuickSort.h>
 #include <Jolt/Core/QuickSort.h>
+#include <Jolt/Core/UnorderedMap.h>
+#include <Jolt/Core/UnorderedSet.h>
 
 
 JPH_NAMESPACE_BEGIN
 JPH_NAMESPACE_BEGIN
 
 
@@ -40,6 +42,26 @@ JPH_IMPLEMENT_SERIALIZABLE_NON_VIRTUAL(SoftBodySharedSettings::Volume)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::Volume, mCompliance)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::Volume, mCompliance)
 }
 }
 
 
+JPH_IMPLEMENT_SERIALIZABLE_NON_VIRTUAL(SoftBodySharedSettings::InvBind)
+{
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::InvBind, mJointIndex)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::InvBind, mInvBind)
+}
+
+JPH_IMPLEMENT_SERIALIZABLE_NON_VIRTUAL(SoftBodySharedSettings::SkinWeight)
+{
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::SkinWeight, mInvBindIndex)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::SkinWeight, mWeight)
+}
+
+JPH_IMPLEMENT_SERIALIZABLE_NON_VIRTUAL(SoftBodySharedSettings::Skinned)
+{
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::Skinned, mVertex)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::Skinned, mWeights)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::Skinned, mMaxDistance)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::Skinned, mBackStop)
+}
+
 JPH_IMPLEMENT_SERIALIZABLE_NON_VIRTUAL(SoftBodySharedSettings)
 JPH_IMPLEMENT_SERIALIZABLE_NON_VIRTUAL(SoftBodySharedSettings)
 {
 {
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mVertices)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mVertices)
@@ -47,6 +69,8 @@ JPH_IMPLEMENT_SERIALIZABLE_NON_VIRTUAL(SoftBodySharedSettings)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mEdgeConstraints)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mEdgeConstraints)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mEdgeGroupEndIndices)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mEdgeGroupEndIndices)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mVolumeConstraints)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mVolumeConstraints)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mSkinnedConstraints)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mInvBindMatrices)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mMaterials)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mMaterials)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mVertexRadius)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mVertexRadius)
 }
 }
@@ -77,6 +101,54 @@ void SoftBodySharedSettings::CalculateVolumeConstraintVolumes()
 	}
 	}
 }
 }
 
 
+void SoftBodySharedSettings::CalculateSkinnedConstraintNormals()
+{
+	// Clear any previous results
+	mSkinnedConstraintNormals.clear();
+
+	// If there are no skinned constraints, we're done
+	if (mSkinnedConstraints.empty())
+		return;
+
+	// First collect all vertices that are skinned
+	UnorderedSet<uint32> skinned_vertices;
+	skinned_vertices.reserve(mSkinnedConstraints.size());
+	for (const Skinned &s : mSkinnedConstraints)
+		skinned_vertices.insert(s.mVertex);
+
+	// Now collect all faces that connect only to skinned vertices
+	UnorderedMap<uint32, UnorderedSet<uint32>> connected_faces;
+	connected_faces.reserve(mVertices.size());
+	for (const Face &f : mFaces)
+	{
+		// Must connect to only skinned vertices
+		bool valid = true;
+		for (uint32 v : f.mVertex)
+			valid &= skinned_vertices.find(v) != skinned_vertices.end();
+		if (!valid)
+			continue;
+
+		// Store faces that connect to vertices
+		for (uint32 v : f.mVertex)
+			connected_faces[v].insert(uint32(&f - mFaces.data()));
+	}
+
+	// Populate the list of connecting faces per skinned vertex
+	mSkinnedConstraintNormals.reserve(mFaces.size());
+	for (Skinned &s : mSkinnedConstraints)
+	{
+		uint32 start = uint32(mSkinnedConstraintNormals.size());
+		JPH_ASSERT((start >> 24) == 0);
+		const UnorderedSet<uint32> &faces = connected_faces[s.mVertex];
+		uint32 num = uint32(faces.size());
+		JPH_ASSERT(num < 256);
+		mSkinnedConstraintNormals.insert(mSkinnedConstraintNormals.end(), faces.begin(), faces.end());
+		QuickSort(mSkinnedConstraintNormals.begin() + start, mSkinnedConstraintNormals.begin() + start + num);
+		s.mNormalInfo = start + (num << 24);
+	}
+	mSkinnedConstraintNormals.shrink_to_fit();
+}
+
 void SoftBodySharedSettings::Optimize(OptimizationResults &outResults)
 void SoftBodySharedSettings::Optimize(OptimizationResults &outResults)
 {
 {
 	const uint cMaxNumGroups = 32;
 	const uint cMaxNumGroups = 32;
@@ -143,7 +215,17 @@ void SoftBodySharedSettings::SaveBinaryState(StreamOut &inStream) const
 	inStream.Write(mEdgeConstraints);
 	inStream.Write(mEdgeConstraints);
 	inStream.Write(mEdgeGroupEndIndices);
 	inStream.Write(mEdgeGroupEndIndices);
 	inStream.Write(mVolumeConstraints);
 	inStream.Write(mVolumeConstraints);
+	inStream.Write(mSkinnedConstraints);
+	inStream.Write(mSkinnedConstraintNormals);
 	inStream.Write(mVertexRadius);
 	inStream.Write(mVertexRadius);
+
+	// Can't write mInvBindMatrices directly because the class contains padding
+	inStream.Write(uint32(mInvBindMatrices.size()));
+	for (const InvBind &ib : mInvBindMatrices)
+	{
+		inStream.Write(ib.mJointIndex);
+		inStream.Write(ib.mInvBind);
+	}
 }
 }
 
 
 void SoftBodySharedSettings::RestoreBinaryState(StreamIn &inStream)
 void SoftBodySharedSettings::RestoreBinaryState(StreamIn &inStream)
@@ -153,7 +235,18 @@ void SoftBodySharedSettings::RestoreBinaryState(StreamIn &inStream)
 	inStream.Read(mEdgeConstraints);
 	inStream.Read(mEdgeConstraints);
 	inStream.Read(mEdgeGroupEndIndices);
 	inStream.Read(mEdgeGroupEndIndices);
 	inStream.Read(mVolumeConstraints);
 	inStream.Read(mVolumeConstraints);
+	inStream.Read(mSkinnedConstraints);
+	inStream.Read(mSkinnedConstraintNormals);
 	inStream.Read(mVertexRadius);
 	inStream.Read(mVertexRadius);
+
+	uint32 num_inv_bind_matrices = 0;
+	inStream.Read(num_inv_bind_matrices);
+	mInvBindMatrices.resize(num_inv_bind_matrices);
+	for (InvBind &ib : mInvBindMatrices)
+	{
+		inStream.Read(ib.mJointIndex);
+		inStream.Read(ib.mInvBind);
+	}
 }
 }
 
 
 void SoftBodySharedSettings::SaveWithMaterials(StreamOut &inStream, SharedSettingsToIDMap &ioSettingsMap, MaterialToIDMap &ioMaterialMap) const
 void SoftBodySharedSettings::SaveWithMaterials(StreamOut &inStream, SharedSettingsToIDMap &ioSettingsMap, MaterialToIDMap &ioMaterialMap) const

+ 39 - 0
Jolt/Physics/SoftBody/SoftBodySharedSettings.h

@@ -23,6 +23,9 @@ public:
 	/// Calculates the initial volume of all tetrahedra of this soft body
 	/// Calculates the initial volume of all tetrahedra of this soft body
 	void				CalculateVolumeConstraintVolumes();
 	void				CalculateVolumeConstraintVolumes();
 
 
+	/// Calculate information needed to be able to calculate the skinned constraint normals at run-time
+	void				CalculateSkinnedConstraintNormals();
+
 	/// Information about the optimization of the soft body, the indices of certain elements may have changed.
 	/// Information about the optimization of the soft body, the indices of certain elements may have changed.
 	class OptimizationResults
 	class OptimizationResults
 	{
 	{
@@ -113,6 +116,39 @@ public:
 		float			mCompliance = 0.0f;							///< Inverse of the stiffness of the constraint
 		float			mCompliance = 0.0f;							///< Inverse of the stiffness of the constraint
 	};
 	};
 
 
+	/// An inverse bind matrix take a skinned vertex from its bind pose into joint local space
+	class JPH_EXPORT InvBind
+	{
+		JPH_DECLARE_SERIALIZABLE_NON_VIRTUAL(JPH_EXPORT, InvBind)
+
+	public:
+		uint32			mJointIndex = 0;							///< Joint index to which this is attached
+		Mat44			mInvBind = Mat44::sIdentity();				///< The inverse bind matrix, this takes a vertex in its bind pose (Vertex::mPosition) to joint local space
+	};
+
+	/// A joint and its skin weight
+	class JPH_EXPORT SkinWeight
+	{
+		JPH_DECLARE_SERIALIZABLE_NON_VIRTUAL(JPH_EXPORT, SkinWeight)
+
+	public:
+		uint32			mInvBindIndex = 0;							///< Index in mInvBindMatrices
+		float			mWeight = 0.0f;								///< Weight with which it is skinned
+	};
+
+	/// A constraint that skins a vertex to joints and limits the distance that the simulated vertex can travel from this vertex
+	class JPH_EXPORT Skinned
+	{
+		JPH_DECLARE_SERIALIZABLE_NON_VIRTUAL(JPH_EXPORT, Skinned)
+
+	public:
+		uint32			mVertex = 0;								///< Index in mVertices which indicates which vertex is being skinned
+		SkinWeight		mWeights[4];								///< Skin weights, the bind pose of the vertex is assumed to be stored in Vertex::mPosition
+		float			mMaxDistance = FLT_MAX;						///< Maximum distance that this vertex can reach from the skinned vertex, disabled when FLT_MAX
+		float			mBackStop = FLT_MAX;						///< Distance how far behind the skin the vertex can move behind the plane formed by the vertex and the normals of its surrounding faces, if it is bigger than mMaxDistance it is disabled
+		uint32			mNormalInfo = 0;							///< Information needed to calculate the normal of this vertex, lowest 24 bit is start index in mSkinnedConstraintNormals, highest 8 bit is number of faces (generated by CalculateSkinnedConstraintNormals())
+	};
+
 	/// Add a face to this soft body
 	/// Add a face to this soft body
 	void				AddFace(const Face &inFace)					{ JPH_ASSERT(!inFace.IsDegenerate()); mFaces.push_back(inFace); }
 	void				AddFace(const Face &inFace)					{ JPH_ASSERT(!inFace.IsDegenerate()); mFaces.push_back(inFace); }
 
 
@@ -124,6 +160,9 @@ public:
 	Array<Edge>			mEdgeConstraints;							///< The list of edges or springs of the body
 	Array<Edge>			mEdgeConstraints;							///< The list of edges or springs of the body
 	Array<uint>			mEdgeGroupEndIndices;						///< The start index of each group of edges that can be solved in parallel
 	Array<uint>			mEdgeGroupEndIndices;						///< The start index of each group of edges that can be solved in parallel
 	Array<Volume>		mVolumeConstraints;							///< The list of volume constraints of the body that keep the volume of tetrahedra in the soft body constant
 	Array<Volume>		mVolumeConstraints;							///< The list of volume constraints of the body that keep the volume of tetrahedra in the soft body constant
+	Array<Skinned>		mSkinnedConstraints;						///< The list of vertices that are constrained to a skinned vertex
+	Array<uint32>		mSkinnedConstraintNormals;					///< A list of indices in the mFaces array used by mSkinnedConstraints and calculated by CalculateSkinnedConstraintNormals()
+	Array<InvBind>		mInvBindMatrices;							///< The list of inverse bind matrices for skinning vertices
 	PhysicsMaterialList mMaterials { PhysicsMaterial::sDefault };	///< The materials of the faces of the body, referenced by Face::mMaterialIndex
 	PhysicsMaterialList mMaterials { PhysicsMaterial::sDefault };	///< The materials of the faces of the body, referenced by Face::mMaterialIndex
 	float				mVertexRadius = 0.0f;						///< How big the particles are, can be used to push the vertices a little bit away from the surface of other bodies to prevent z-fighting
 	float				mVertexRadius = 0.0f;						///< How big the particles are, can be used to push the vertices a little bit away from the surface of other bodies to prevent z-fighting
 };
 };