Browse Source

Added support for soft body bend constraints (#1010)

Jorrit Rouwe 1 year ago
parent
commit
8e4bf3fa03

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

@@ -1085,6 +1085,9 @@ void BodyManager::Draw(const DrawSettings &inDrawSettings, const PhysicsSettings
 				if (inDrawSettings.mDrawSoftBodyEdgeConstraints)
 					mp->DrawEdgeConstraints(inRenderer, com);
 
+				if (inDrawSettings.mDrawSoftBodyBendConstraints)
+					mp->DrawBendConstraints(inRenderer, com);
+
 				if (inDrawSettings.mDrawSoftBodyVolumeConstraints)
 					mp->DrawVolumeConstraints(inRenderer, com);
 

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

@@ -232,6 +232,7 @@ public:
 		bool						mDrawSoftBodyVertices = false;					///< Draw the vertices of soft bodies
 		bool						mDrawSoftBodyVertexVelocities = false;			///< Draw the velocities of the vertices of soft bodies
 		bool						mDrawSoftBodyEdgeConstraints = false;			///< Draw the edge constraints of soft bodies
+		bool						mDrawSoftBodyBendConstraints = false;			///< Draw the bend 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						mDrawSoftBodyLRAConstraints = false;			///< Draw the LRA constraints of soft bodies

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

@@ -15,6 +15,8 @@
 
 JPH_NAMESPACE_BEGIN
 
+using namespace JPH::literals;
+
 void SoftBodyMotionProperties::CalculateMassAndInertia()
 {
 	MassProperties mp;
@@ -246,6 +248,139 @@ void SoftBodyMotionProperties::IntegratePositions(const SoftBodyUpdateContext &i
 		}
 }
 
+void SoftBodyMotionProperties::ApplyBendConstraints(const SoftBodyUpdateContext &inContext)
+{
+	JPH_PROFILE_FUNCTION();
+
+	float inv_dt_sq = 1.0f / Square(inContext.mSubStepDeltaTime);
+
+	for (const IsometricBend &b : mSettings->mIsometricBendConstraints)
+	{
+		Vertex &v0 = mVertices[b.mVertex[0]];
+		Vertex &v1 = mVertices[b.mVertex[1]];
+		Vertex &v2 = mVertices[b.mVertex[2]];
+		Vertex &v3 = mVertices[b.mVertex[3]];
+
+		// Get positions
+		// Setting x0 as origin
+		Vec3 x1 = v1.mPosition - v0.mPosition;
+		Vec3 x2 = v2.mPosition - v0.mPosition;
+		Vec3 x3 = v3.mPosition - v0.mPosition;
+
+		// Calculate constraint equation
+		// C = 0.5 * Sum_i,j(Q_ij * x_i . x_j)
+		// Note that Q is symmetric so we can optimize this to:
+		float c = b.mQ12 * x1.Dot(x2) + b.mQ13 * x1.Dot(x3) + b.mQ23 * x2.Dot(x3) // Off diagonal elements occur twice
+				+ 0.5f * (b.mQ11 * x1.LengthSq() + b.mQ22 * x2.LengthSq() + b.mQ33 * x3.LengthSq()); // Diagonal elements only once
+		if (abs(c) < 1.0e-6f)
+			continue;
+
+		// Calculate gradient of constraint equation, again using Q_ij = Q_ji
+		// del C = Sum_j(Q_ij * x_j)
+		Vec3 d0c = b.mQ01 * x1 + b.mQ02 * x2 + b.mQ03 * x3;
+		Vec3 d1c = b.mQ11 * x1 + b.mQ12 * x2 + b.mQ13 * x3;
+		Vec3 d2c = b.mQ12 * x1 + b.mQ22 * x2 + b.mQ23 * x3;
+		Vec3 d3c = -d0c - d1c - d2c; // The sum of the gradients needs to be zero
+
+		// Get masses
+		float w0 = v0.mInvMass;
+		float w1 = v1.mInvMass;
+		float w2 = v2.mInvMass;
+		float w3 = v3.mInvMass;
+		JPH_ASSERT(w0 > 0.0f || w1 > 0.0f || w2 > 0.0f || w3 > 0.0f);
+
+		// Apply correction
+		float denom = w0 * d0c.LengthSq() + w1 * d1c.LengthSq() + w2 * d2c.LengthSq() + w3 * d3c.LengthSq() + b.mCompliance * inv_dt_sq;
+		if (denom == 0.0f)
+			continue;
+		float lambda = -c / denom;
+		v0.mPosition += lambda * w0 * d0c;
+		v1.mPosition += lambda * w1 * d1c;
+		v2.mPosition += lambda * w2 * d2c;
+		v3.mPosition += lambda * w3 * d3c;
+	}
+
+	for (const DihedralBend &b : mSettings->mDihedralBendConstraints)
+	{
+		Vertex &v0 = mVertices[b.mVertex[0]];
+		Vertex &v1 = mVertices[b.mVertex[1]];
+		Vertex &v2 = mVertices[b.mVertex[2]];
+		Vertex &v3 = mVertices[b.mVertex[3]];
+
+		// Get positions
+		Vec3 x0 = v0.mPosition;
+		Vec3 x1 = v1.mPosition;
+		Vec3 x2 = v2.mPosition;
+		Vec3 x3 = v3.mPosition;
+
+		/*
+		   x2
+		e1/  \e3
+		 /    \
+		x0----x1
+		 \ e0 /
+		e2\  /e4
+		   x3
+		*/
+
+		// Calculate the shared edge of the triangles
+		Vec3 e = x1 - x0;
+		float e_len = e.Length();
+		if (e_len < 1.0e-6f)
+			continue;
+
+		// Calculate the normals of the triangles
+		Vec3 x1x2 = x2 - x1;
+		Vec3 x1x3 = x3 - x1;
+		Vec3 n1 = (x2 - x0).Cross(x1x2);
+		Vec3 n2 = x1x3.Cross(x3 - x0);
+		float n1_len_sq = n1.LengthSq();
+		float n2_len_sq = n2.LengthSq();
+		if (n1_len_sq < 1.0e-12f || n2_len_sq < 1.0e-12f)
+			continue;
+
+		// Calculate constraint equation
+		float d = n1.Dot(n2) / sqrt(n1_len_sq * n2_len_sq);
+		float c = ACos(d) - b.mInitialAngle;
+
+		// Calculate gradient of constraint equation
+		// Taken from "Strain Based Dynamics" - Matthias Muller et al. (Appendix A)
+		// with p1 = x2, p2 = x3, p3 = x0 and p4 = x1
+		// which in turn is based on "Simulation of Clothing with Folds and Wrinkles" - R. Bridson et al. (Section 4)
+		n1 /= n1_len_sq;
+		n2 /= n2_len_sq;
+		Vec3 d0c = (x1x2.Dot(e) * n1 + x1x3.Dot(e) * n2) / e_len;
+		Vec3 d2c = e_len * n1;
+		Vec3 d3c = e_len * n2;
+		
+		// The sum of the gradients must be zero (see "Strain Based Dynamics" section 4)
+		Vec3 d1c = -d0c - d2c - d3c;
+
+		// Get masses
+		float w0 = v0.mInvMass;
+		float w1 = v1.mInvMass;
+		float w2 = v2.mInvMass;
+		float w3 = v3.mInvMass;
+		JPH_ASSERT(w0 > 0.0f || w1 > 0.0f || w2 > 0.0f || w3 > 0.0f);
+
+		// Calculate -lambda
+		float denom = w0 * d0c.LengthSq() + w1 * d1c.LengthSq() + w2 * d2c.LengthSq() + w3 * d3c.LengthSq() + b.mCompliance * inv_dt_sq;
+		if (denom < 1.0e-12f)
+			continue;
+		float minus_lambda = c / denom;
+
+		// Negate the gradients (in this case we apply the sign flip in lambda) as per "Strain Based Dynamics" Appendix A
+		if (n1.Cross(n2).Dot(e) > 0.0f)
+			minus_lambda = -minus_lambda;
+
+		// Apply correction
+		v0.mPosition -= minus_lambda * w0 * d0c;
+		v1.mPosition -= minus_lambda * w1 * d1c;
+		v2.mPosition -= minus_lambda * w2 * d2c;
+		v3.mPosition -= minus_lambda * w3 * d3c;
+	}
+}
+
 void SoftBodyMotionProperties::ApplyVolumeConstraints(const SoftBodyUpdateContext &inContext)
 {
 	JPH_PROFILE_FUNCTION();
@@ -277,6 +412,7 @@ void SoftBodyMotionProperties::ApplyVolumeConstraints(const SoftBodyUpdateContex
 		Vec3 d3c = x1x4.Cross(x1x2);
 		Vec3 d4c = x1x2.Cross(x1x3);
 
+		// Get masses
 		float w1 = v1.mInvMass;
 		float w2 = v2.mInvMass;
 		float w3 = v3.mInvMass;
@@ -634,6 +770,8 @@ void SoftBodyMotionProperties::StartNextIteration(const SoftBodyUpdateContext &i
 
 	IntegratePositions(ioContext);
 
+	ApplyBendConstraints(ioContext);
+
 	ApplyVolumeConstraints(ioContext);
 }
 
@@ -879,6 +1017,39 @@ void SoftBodyMotionProperties::DrawEdgeConstraints(DebugRenderer *inRenderer, RM
 		inRenderer->DrawLine(inCenterOfMassTransform * mVertices[e.mVertex[0]].mPosition, inCenterOfMassTransform * mVertices[e.mVertex[1]].mPosition, Color::sWhite);
 }
 
+void SoftBodyMotionProperties::DrawBendConstraints(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const
+{
+	for (const IsometricBend &b : mSettings->mIsometricBendConstraints)
+	{
+		RVec3 x0 = inCenterOfMassTransform * mVertices[b.mVertex[0]].mPosition;
+		RVec3 x1 = inCenterOfMassTransform * mVertices[b.mVertex[1]].mPosition;
+		RVec3 x2 = inCenterOfMassTransform * mVertices[b.mVertex[2]].mPosition;
+		RVec3 x3 = inCenterOfMassTransform * mVertices[b.mVertex[3]].mPosition;
+		RVec3 c_edge = 0.5_r * (x0 + x1);
+		RVec3 c0 = (x0 + x1 + x2) / 3.0_r;
+		RVec3 c1 = (x0 + x1 + x3) / 3.0_r;
+
+		inRenderer->DrawArrow(0.9_r * x0 + 0.1_r * x1, 0.1_r * x0 + 0.9_r * x1, Color::sDarkGreen, 0.01f);
+		inRenderer->DrawLine(c_edge, 0.1_r * c_edge + 0.9_r * c0, Color::sGreen);
+		inRenderer->DrawLine(c_edge, 0.1_r * c_edge + 0.9_r * c1, Color::sGreen);
+	}
+
+	for (const DihedralBend &b : mSettings->mDihedralBendConstraints)
+	{
+		RVec3 x0 = inCenterOfMassTransform * mVertices[b.mVertex[0]].mPosition;
+		RVec3 x1 = inCenterOfMassTransform * mVertices[b.mVertex[1]].mPosition;
+		RVec3 x2 = inCenterOfMassTransform * mVertices[b.mVertex[2]].mPosition;
+		RVec3 x3 = inCenterOfMassTransform * mVertices[b.mVertex[3]].mPosition;
+		RVec3 c_edge = 0.5_r * (x0 + x1);
+		RVec3 c0 = (x0 + x1 + x2) / 3.0_r;
+		RVec3 c1 = (x0 + x1 + x3) / 3.0_r;
+
+		inRenderer->DrawArrow(0.9_r * x0 + 0.1_r * x1, 0.1_r * x0 + 0.9_r * x1, Color::sDarkOrange, 0.01f);
+		inRenderer->DrawLine(c_edge, 0.1_r * c_edge + 0.9_r * c0, Color::sOrange);
+		inRenderer->DrawLine(c_edge, 0.1_r * c_edge + 0.9_r * c1, Color::sOrange);
+	}
+}
+
 void SoftBodyMotionProperties::DrawVolumeConstraints(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const
 {
 	for (const Volume &v : mSettings->mVolumeConstraints)

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

@@ -35,6 +35,8 @@ public:
 	using Vertex = SoftBodyVertex;
 	using Edge = SoftBodySharedSettings::Edge;
 	using Face = SoftBodySharedSettings::Face;
+	using IsometricBend = SoftBodySharedSettings::IsometricBend;
+	using DihedralBend = SoftBodySharedSettings::DihedralBend;
 	using Volume = SoftBodySharedSettings::Volume;
 	using InvBind = SoftBodySharedSettings::InvBind;
 	using SkinWeight = SoftBodySharedSettings::SkinWeight;
@@ -90,6 +92,7 @@ public:
 	void								DrawVertices(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const;
 	void								DrawVertexVelocities(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const;
 	void								DrawEdgeConstraints(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const;
+	void								DrawBendConstraints(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const;
 	void								DrawVolumeConstraints(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const;
 	void								DrawSkinConstraints(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const;
 	void								DrawLRAConstraints(DebugRenderer *inRenderer, RMat44Arg inCenterOfMassTransform) const;
@@ -193,6 +196,9 @@ private:
 	/// Integrate the positions of all vertices by 1 sub step
 	void								IntegratePositions(const SoftBodyUpdateContext &inContext);
 
+	/// Enforce all bend constraints
+	void								ApplyBendConstraints(const SoftBodyUpdateContext &inContext);
+
 	/// Enforce all volume constraints
 	void								ApplyVolumeConstraints(const SoftBodyUpdateContext &inContext);
 

+ 197 - 14
Jolt/Physics/SoftBody/SoftBodySharedSettings.cpp

@@ -35,6 +35,28 @@ JPH_IMPLEMENT_SERIALIZABLE_NON_VIRTUAL(SoftBodySharedSettings::Edge)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::Edge, mCompliance)
 }
 
+JPH_IMPLEMENT_SERIALIZABLE_NON_VIRTUAL(SoftBodySharedSettings::IsometricBend)
+{
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::IsometricBend, mVertex)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::IsometricBend, mCompliance)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::IsometricBend, mQ01)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::IsometricBend, mQ02)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::IsometricBend, mQ03)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::IsometricBend, mQ11)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::IsometricBend, mQ12)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::IsometricBend, mQ13)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::IsometricBend, mQ22)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::IsometricBend, mQ23)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::IsometricBend, mQ33)
+}
+
+JPH_IMPLEMENT_SERIALIZABLE_NON_VIRTUAL(SoftBodySharedSettings::DihedralBend)
+{
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::DihedralBend, mVertex)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::DihedralBend, mCompliance)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::DihedralBend, mInitialAngle)
+}
+
 JPH_IMPLEMENT_SERIALIZABLE_NON_VIRTUAL(SoftBodySharedSettings::Volume)
 {
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings::Volume, mVertex)
@@ -75,6 +97,8 @@ JPH_IMPLEMENT_SERIALIZABLE_NON_VIRTUAL(SoftBodySharedSettings)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mFaces)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mEdgeConstraints)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mEdgeGroupEndIndices)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mIsometricBendConstraints)
+	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mDihedralBendConstraints)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mVolumeConstraints)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mSkinnedConstraints)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mInvBindMatrices)
@@ -83,7 +107,7 @@ JPH_IMPLEMENT_SERIALIZABLE_NON_VIRTUAL(SoftBodySharedSettings)
 	JPH_ADD_ATTRIBUTE(SoftBodySharedSettings, mVertexRadius)
 }
 
-void SoftBodySharedSettings::CreateEdges(float inCompliance, float inAngleTolerance)
+void SoftBodySharedSettings::CreateConstraints(const VertexAttributes *inVertexAttributes, uint inVertexAttributesLength, EBendType inBendType, float inAngleTolerance)
 {
 	struct EdgeHelper
 	{
@@ -111,19 +135,25 @@ void SoftBodySharedSettings::CreateEdges(float inCompliance, float inAngleTolera
 	QuickSort(edges.begin(), edges.end(), [](const EdgeHelper &inLHS, const EdgeHelper &inRHS) { return inLHS.mVertex[0] < inRHS.mVertex[0] || (inLHS.mVertex[0] == inRHS.mVertex[0] && inLHS.mVertex[1] < inRHS.mVertex[1]); });
 
 	// Only add edges if one of the vertices is movable
-	auto add_edge = [this, inCompliance](uint32 inVtx1, uint32 inVtx2) {
-		if (mVertices[inVtx1].mInvMass > 0.0f || mVertices[inVtx2].mInvMass > 0.0f)
+	auto add_edge = [this](uint32 inVtx1, uint32 inVtx2, float inCompliance1, float inCompliance2) {
+		if ((mVertices[inVtx1].mInvMass > 0.0f || mVertices[inVtx2].mInvMass > 0.0f)
+			&& inCompliance1 < FLT_MAX && inCompliance2 < FLT_MAX)
 		{
 			Edge temp_edge;
 			temp_edge.mVertex[0] = inVtx1;
 			temp_edge.mVertex[1] = inVtx2;
-			temp_edge.mCompliance = inCompliance;
+			temp_edge.mCompliance = 0.5f * (inCompliance1 + inCompliance2);
 			temp_edge.mRestLength = (Vec3(mVertices[inVtx2].mPosition) - Vec3(mVertices[inVtx1].mPosition)).Length();
 			JPH_ASSERT(temp_edge.mRestLength > 0.0f);
 			mEdgeConstraints.push_back(temp_edge);
 		}
 	};
 
+	// Helper function to get the attributes of a vertex
+	auto attr = [inVertexAttributes, inVertexAttributesLength](uint32 inVertex) {
+		return inVertexAttributes[min(inVertex, inVertexAttributesLength - 1)];
+	};
+
 	// Create the constraints
 	float sq_sin_tolerance = Square(Sin(inAngleTolerance));
 	float sq_cos_tolerance = Square(Cos(inAngleTolerance));
@@ -133,8 +163,8 @@ void SoftBodySharedSettings::CreateEdges(float inCompliance, float inAngleTolera
 	{
 		const EdgeHelper &e0 = edges[i];
 
-		// Create a regular edge constraint
-		add_edge(e0.mVertex[0], e0.mVertex[1]);
+		// Flag that indicates if this edge is a shear edge (if 2 triangles form a quad-like shape and this edge is on the diagonal)
+		bool is_shear = false;
 
 		// Test if there are any shared edges
 		for (Array<EdgeHelper>::size_type j = i + 1; j < edges.size(); ++j)
@@ -142,26 +172,73 @@ void SoftBodySharedSettings::CreateEdges(float inCompliance, float inAngleTolera
 			const EdgeHelper &e1 = edges[j];
 			if (e0.mVertex[0] == e1.mVertex[0] && e0.mVertex[1] == e1.mVertex[1])
 			{
-				// Faces should be roughly in a plane
+				// Get opposing vertices
 				const Face &f0 = mFaces[e0.mEdgeIdx / 3];
 				const Face &f1 = mFaces[e1.mEdgeIdx / 3];
+				uint32 vopposite0 = f0.mVertex[(e0.mEdgeIdx + 2) % 3];
+				uint32 vopposite1 = f1.mVertex[(e1.mEdgeIdx + 2) % 3];
+				uint32 v_min = min(vopposite0, vopposite1);
+				uint32 v_max = max(vopposite0, vopposite1);
+				const VertexAttributes &a_min = attr(v_min);
+				const VertexAttributes &a_max = attr(v_max);
+
+				// Faces should be roughly in a plane
 				Vec3 n0 = (Vec3(mVertices[f0.mVertex[2]].mPosition) - Vec3(mVertices[f0.mVertex[0]].mPosition)).Cross(Vec3(mVertices[f0.mVertex[1]].mPosition) - Vec3(mVertices[f0.mVertex[0]].mPosition));
 				Vec3 n1 = (Vec3(mVertices[f1.mVertex[2]].mPosition) - Vec3(mVertices[f1.mVertex[0]].mPosition)).Cross(Vec3(mVertices[f1.mVertex[1]].mPosition) - Vec3(mVertices[f1.mVertex[0]].mPosition));
 				if (Square(n0.Dot(n1)) > sq_cos_tolerance * n0.LengthSq() * n1.LengthSq())
 				{
-					// Get opposing vertices
-					uint32 v0 = f0.mVertex[(e0.mEdgeIdx + 2) % 3];
-					uint32 v1 = f1.mVertex[(e1.mEdgeIdx + 2) % 3];
-
 					// Faces should approximately form a quad
-					Vec3 e0_dir = Vec3(mVertices[v0].mPosition) - Vec3(mVertices[e0.mVertex[0]].mPosition);
-					Vec3 e1_dir = Vec3(mVertices[v1].mPosition) - Vec3(mVertices[e0.mVertex[0]].mPosition);
+					Vec3 e0_dir = Vec3(mVertices[vopposite0].mPosition) - Vec3(mVertices[e0.mVertex[0]].mPosition);
+					Vec3 e1_dir = Vec3(mVertices[vopposite1].mPosition) - Vec3(mVertices[e0.mVertex[0]].mPosition);
 					if (Square(e0_dir.Dot(e1_dir)) < sq_sin_tolerance * e0_dir.LengthSq() * e1_dir.LengthSq())
 					{
 						// Shear constraint
-						add_edge(min(v0, v1), max(v0, v1));
+						add_edge(v_min, v_max, a_min.mShearCompliance, a_max.mShearCompliance);
+						is_shear = true;
 					}
 				}
+
+				// Bend constraint
+				switch (inBendType)
+				{
+				case EBendType::None:
+					// Do nothing
+					break;
+
+				case EBendType::Distance:
+					// Create an edge constraint to represent the bend constraint
+					if (!is_shear)
+						add_edge(v_min, v_max, a_min.mBendCompliance, a_max.mBendCompliance);
+					break;
+
+				case EBendType::Isometric:
+					// Test if both opposite vertices are free to move
+					if ((mVertices[vopposite0].mInvMass > 0.0f || mVertices[vopposite1].mInvMass > 0.0f)
+						&& a_min.mBendCompliance < FLT_MAX && a_max.mBendCompliance < FLT_MAX)
+					{
+						// Get the vertices that form the common edge
+						uint32 vedge0 = f0.mVertex[e0.mEdgeIdx % 3];
+						uint32 vedge1 = f0.mVertex[(e0.mEdgeIdx + 1) % 3];
+
+						// Create a bend constraint
+						mIsometricBendConstraints.emplace_back(vedge0, vedge1, vopposite0, vopposite1, 0.5f * (a_min.mBendCompliance + a_max.mBendCompliance));
+					}
+					break;
+
+				case EBendType::Dihedral:
+					// Test if both opposite vertices are free to move
+					if ((mVertices[vopposite0].mInvMass > 0.0f || mVertices[vopposite1].mInvMass > 0.0f)
+						&& a_min.mBendCompliance < FLT_MAX && a_max.mBendCompliance < FLT_MAX)
+					{
+						// Get the vertices that form the common edge
+						uint32 vedge0 = f0.mVertex[e0.mEdgeIdx % 3];
+						uint32 vedge1 = f0.mVertex[(e0.mEdgeIdx + 1) % 3];
+
+						// Create a bend constraint
+						mDihedralBendConstraints.emplace_back(vedge0, vedge1, vopposite0, vopposite1, 0.5f * (a_min.mBendCompliance + a_max.mBendCompliance));
+					}
+					break;
+				}
 			}
 			else
 			{
@@ -170,8 +247,15 @@ void SoftBodySharedSettings::CreateEdges(float inCompliance, float inAngleTolera
 				break;
 			}
 		}
+
+		// Create a edge constraint for the current edge
+		const VertexAttributes &a0 = attr(e0.mVertex[0]);
+		const VertexAttributes &a1 = attr(e0.mVertex[1]);
+		add_edge(e0.mVertex[0], e0.mVertex[1], is_shear? a0.mShearCompliance : a0.mCompliance, is_shear? a1.mShearCompliance : a1.mCompliance);
 	}
 	mEdgeConstraints.shrink_to_fit();
+
+	CalculateBendConstraintConstants();
 }
 
 void SoftBodySharedSettings::CalculateEdgeLengths()
@@ -192,6 +276,101 @@ void SoftBodySharedSettings::CalculateLRALengths()
 	}
 }
 
+static float sCotangent(Vec3Arg inV1, Vec3Arg inV2)
+{
+	float dot = inV1.Dot(inV2);
+	float cross = inV1.Cross(inV2).Length();
+	return dot / cross;
+}
+
+void SoftBodySharedSettings::CalculateBendConstraintConstants()
+{
+	for (IsometricBend &b : mIsometricBendConstraints)
+	{
+		// Get positions
+		Vec3 x0 = Vec3(mVertices[b.mVertex[0]].mPosition);
+		Vec3 x1 = Vec3(mVertices[b.mVertex[1]].mPosition);
+		Vec3 x2 = Vec3(mVertices[b.mVertex[2]].mPosition);
+		Vec3 x3 = Vec3(mVertices[b.mVertex[3]].mPosition);
+
+		/*
+		Figure in section 2 of "A Quadratic Bending Model for Inextensible Surfaces"
+		   x2
+		e1/  \e3
+		 /    \
+		x0----x1
+		 \ e0 /
+		e2\  /e4
+		   x3
+		*/
+
+		// Calculate edges
+		Vec3 e0 = x1 - x0;
+		Vec3 e1 = x2 - x0;
+		Vec3 e2 = x3 - x0;
+		Vec3 e3 = x2 - x1;
+		Vec3 e4 = x3 - x1;
+
+		// Calculate cotangents
+		float c02 = sCotangent(e0, e2);
+		float c03 = sCotangent(-e0, e3);
+		float c04 = sCotangent(-e0, e4);
+		float c01 = sCotangent(e0, e1);
+
+		// 2x area of both triangles
+		float two_a0 = e0.Cross(e1).Length();
+		float two_a1 = e0.Cross(e2).Length();
+
+		// Calculate Q, note that this matrix is symmetric so we don't need to store all elements
+		Vec4 k0(c03 + c04, c01 + c02, -c01 - c03, -c02 - c04);
+		Mat44 k0_dot_k0_t(k0.GetX() * k0, k0.GetY() * k0, k0.GetZ() * k0, k0.GetW() * k0);
+		Mat44 q = (6.0f / (two_a0 + two_a1)) * k0_dot_k0_t;
+		// q00 is not used since we set x0 to be the origin
+		b.mQ01 = q(0, 1);
+		b.mQ02 = q(0, 2);
+		b.mQ03 = q(0, 3);
+		b.mQ11 = q(1, 1);
+		b.mQ12 = q(1, 2);
+		b.mQ13 = q(1, 3);
+		b.mQ22 = q(2, 2);
+		b.mQ23 = q(2, 3);
+		b.mQ33 = q(3, 3);
+	}
+
+	for (DihedralBend &b : mDihedralBendConstraints)
+	{
+		// Get positions
+		Vec3 x0 = Vec3(mVertices[b.mVertex[0]].mPosition);
+		Vec3 x1 = Vec3(mVertices[b.mVertex[1]].mPosition);
+		Vec3 x2 = Vec3(mVertices[b.mVertex[2]].mPosition);
+		Vec3 x3 = Vec3(mVertices[b.mVertex[3]].mPosition);
+
+		/*
+		   x2
+		e1/  \e3
+		 /    \
+		x0----x1
+		 \ e0 /
+		e2\  /e4
+		   x3
+		*/
+
+		// Calculate edges
+		Vec3 e0 = x1 - x0;
+		Vec3 e1 = x2 - x0;
+		Vec3 e2 = x3 - x0;
+
+		// Normals of both triangles
+		Vec3 n0 = e0.Cross(e1);
+		Vec3 n1 = e2.Cross(e0);
+		float denom = sqrt(n0.LengthSq() * n1.LengthSq());
+		if (denom == 0.0f)
+			b.mInitialAngle = 0.0f;
+		else
+			b.mInitialAngle = ACos(n0.Dot(n1) / denom);
+	}
+}
+
 void SoftBodySharedSettings::CalculateVolumeConstraintVolumes()
 {
 	for (Volume &v : mVolumeConstraints)
@@ -339,6 +518,8 @@ void SoftBodySharedSettings::SaveBinaryState(StreamOut &inStream) const
 	inStream.Write(mFaces);
 	inStream.Write(mEdgeConstraints);
 	inStream.Write(mEdgeGroupEndIndices);
+	inStream.Write(mIsometricBendConstraints);
+	inStream.Write(mDihedralBendConstraints);
 	inStream.Write(mVolumeConstraints);
 	inStream.Write(mSkinnedConstraints);
 	inStream.Write(mSkinnedConstraintNormals);
@@ -358,6 +539,8 @@ void SoftBodySharedSettings::RestoreBinaryState(StreamIn &inStream)
 	inStream.Read(mFaces);
 	inStream.Read(mEdgeConstraints);
 	inStream.Read(mEdgeGroupEndIndices);
+	inStream.Read(mIsometricBendConstraints);
+	inStream.Read(mDihedralBendConstraints);
 	inStream.Read(mVolumeConstraints);
 	inStream.Read(mSkinnedConstraints);
 	inStream.Read(mSkinnedConstraintNormals);

+ 98 - 5
Jolt/Physics/SoftBody/SoftBodySharedSettings.h

@@ -17,17 +17,43 @@ class JPH_EXPORT SoftBodySharedSettings : public RefTarget<SoftBodySharedSetting
 public:
 	JPH_DECLARE_SERIALIZABLE_NON_VIRTUAL(JPH_EXPORT, SoftBodySharedSettings)
 
-	/// Automatically create all edges based on the faces
-	/// @param inCompliance The compliance of the edges
+	/// Which type of bend constraint should be created
+	enum class EBendType
+	{
+		None,														///< No bend constraints will be created
+		Distance,													///< A simple distance constraint
+		Isometric,													///< An isometric bend constraint (better but much more expensive)
+		Dihedral,													///< A dihedral bend constraint (most expensive, but also supports triangles that are initially not in the same plane)
+	};
+
+	/// Per vertex attributes used during the CreateConstraints function
+	struct JPH_EXPORT VertexAttributes
+	{
+		/// Constructor
+						VertexAttributes() = default;
+						VertexAttributes(float inCompliance, float inShearCompliance, float inBendCompliance) : mCompliance(inCompliance), mShearCompliance(inShearCompliance), mBendCompliance(inBendCompliance) { }
+
+		float			mCompliance = 0.0f;							///< The compliance of the normal edges. Set to FLT_MAX to disable regular edges.
+		float			mShearCompliance = 0.0f;					///< The compliance of the shear edges. Set to FLT_MAX to disable shear edges.
+		float			mBendCompliance = FLT_MAX;					///< The compliance of the bend edges. Set to FLT_MAX to disable bend edges.
+	};
+
+	/// Automatically create constraints based on the faces of the soft body
+	/// @param inVertexAttributes A list of attributes for each vertex (1-on-1 with mVertices, note that if the list is smaller than mVertices the last element will be repeated). This defines the properties of the constraints that are created.
+	/// @param inVertexAttributesLength The length of inVertexAttributes
 	/// @param inAngleTolerance Shear edges are created when two connected triangles form a quad (are roughly in the same plane and form a square with roughly 90 degree angles). This defines the tolerance (in radians).
-	void				CreateEdges(float inCompliance, float inAngleTolerance = DegreesToRadians(8.0f));
+	/// @param inBendType The type of bend constraint to create
+	void				CreateConstraints(const VertexAttributes *inVertexAttributes, uint inVertexAttributesLength, EBendType inBendType = EBendType::Distance, float inAngleTolerance = DegreesToRadians(8.0f));
 
-	/// Calculate the initial lengths of all springs of the edges of this soft body
+	/// Calculate the initial lengths of all springs of the edges of this soft body (if you use CreateConstraint, this is already done)
 	void				CalculateEdgeLengths();
 
 	/// Calculate the max lengths for the long range attachment constraints
 	void				CalculateLRALengths();
 
+	/// Calculate the Q values for the isometric bend constraints (if you use CreateConstraint, this is already done)
+	void				CalculateBendConstraintConstants();
+
 	/// Calculates the initial volume of all tetrahedra of this soft body
 	void				CalculateVolumeConstraintVolumes();
 
@@ -113,6 +139,71 @@ public:
 		float			mCompliance = 0.0f;							///< Inverse of the stiffness of the spring
 	};
 
+	/**
+	 * An isometric bend constraint keeps the angle between 2 triangles that share an edge constant:
+	 * 
+	 *        x2
+	 *       /  \
+	 *      / t0 \
+	 *     x0----x1
+	 *      \ t1 /
+	 *       \  /
+	 *        x3
+	 * 
+	 * x0..x3 are the vertices, t0 and t1 are the triangles that share the edge x0..x1
+	 * 
+	 * Based on:
+	 * - "Discrete Quadratic Curvature Energies" - Max Wardetzky et al.
+	 * - "A Quadratic Bending Model for Inextensible Surfaces" - Miklos Bergou et al.
+	 * 
+	 * Warning: Only works when the rest pose of the triangles is in a plane. Also assumes no stretching.
+	 */
+	struct JPH_EXPORT IsometricBend
+	{
+		JPH_DECLARE_SERIALIZABLE_NON_VIRTUAL(JPH_EXPORT, IsometricBend)
+
+		/// Constructor
+						IsometricBend() = default;
+						IsometricBend(uint32 inVertex1, uint32 inVertex2, uint32 inVertex3, uint32 inVertex4, float inCompliance = 0.0f) : mVertex { inVertex1, inVertex2, inVertex3, inVertex4 }, mCompliance(inCompliance) { }
+
+		uint32			mVertex[4];									///< Indices of the vertices of the 2 triangles that share an edge (the first 2 vertices are the shared edge)
+		float			mCompliance = 0.0f;							///< Inverse of the stiffness of the constraint
+		float			mQ01 { 0 }, mQ02 { 0 }, mQ03 { 0 };			///< The Q matrix that defines the rest shape of the bend constraint and is calculated by CalculateBendConstraintQs()
+		float			mQ11 { 0 }, mQ12 { 0 }, mQ13 { 0 };
+		float			mQ22 { 0 }, mQ23 { 0 }, mQ33 { 0 };
+	};
+
+	/**
+	 * A dihedral bend constraint
+	 * 
+	 *        x2
+	 *       /  \
+	 *      / t0 \
+	 *     x0----x1
+	 *      \ t1 /
+	 *       \  /
+	 *        x3
+	 * 
+	 * x0..x3 are the vertices, t0 and t1 are the triangles that share the edge x0..x1
+	 * 
+	 * Based on:
+	 * - "Position Based Dynamics" - Matthias Muller et al.
+	 * - "Strain Based Dynamics" - Matthias Muller et al.
+	 * - "Simulation of Clothing with Folds and Wrinkles" - R. Bridson et al.
+	 */
+	struct JPH_EXPORT DihedralBend
+	{
+		JPH_DECLARE_SERIALIZABLE_NON_VIRTUAL(JPH_EXPORT, DihedralBend)
+
+		/// Constructor
+						DihedralBend() = default;
+						DihedralBend(uint32 inVertex1, uint32 inVertex2, uint32 inVertex3, uint32 inVertex4, float inCompliance = 0.0f) : mVertex { inVertex1, inVertex2, inVertex3, inVertex4 }, mCompliance(inCompliance) { }
+
+		uint32			mVertex[4];									///< Indices of the vertices of the 2 triangles that share an edge (the first 2 vertices are the shared edge)
+		float			mCompliance = 0.0f;							///< Inverse of the stiffness of the constraint
+		float			mInitialAngle = 0.0f;						///< Initial angle between the normals of the triangles (pi - dihedral angle).
+	};
+
 	/// Volume constraint, keeps the volume of a tetrahedron constant
 	struct JPH_EXPORT Volume
 	{
@@ -123,7 +214,7 @@ public:
 						Volume(uint32 inVertex1, uint32 inVertex2, uint32 inVertex3, uint32 inVertex4, float inCompliance = 0.0f) : mVertex { inVertex1, inVertex2, inVertex3, inVertex4 }, mCompliance(inCompliance) { }
 
 		uint32			mVertex[4];									///< Indices of the vertices that form the tetrhedron
-		float			mSixRestVolume = 1.0f;						///< 6 times the rest volume of the tetrahedron
+		float			mSixRestVolume = 1.0f;						///< 6 times the rest volume of the tetrahedron (calculated by CalculateVolumeConstraintVolumes())
 		float			mCompliance = 0.0f;							///< Inverse of the stiffness of the constraint
 	};
 
@@ -208,6 +299,8 @@ public:
 	Array<Vertex>		mVertices;									///< The list of vertices or particles of the body
 	Array<Face>			mFaces;										///< The list of faces of the body
 	Array<Edge>			mEdgeConstraints;							///< The list of edges or springs of the body
+	Array<IsometricBend>mIsometricBendConstraints;					///< The list of isometric bend constraints of the body
+	Array<DihedralBend>	mDihedralBendConstraints;					///< The list of dihedral bend constraints of the body
 	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<InvBind>		mInvBindMatrices;							///< The list of inverse bind matrices for skinning vertices

+ 2 - 0
Samples/Samples.cmake

@@ -167,6 +167,8 @@ set(SAMPLES_SRC_FILES
 	${SAMPLES_ROOT}/Tests/Rig/RigPileTest.h
 	${SAMPLES_ROOT}/Tests/Rig/SkeletonMapperTest.cpp
 	${SAMPLES_ROOT}/Tests/Rig/SkeletonMapperTest.h
+	${SAMPLES_ROOT}/Tests/SoftBody/SoftBodyBendConstraintTest.cpp
+	${SAMPLES_ROOT}/Tests/SoftBody/SoftBodyBendConstraintTest.h
 	${SAMPLES_ROOT}/Tests/SoftBody/SoftBodyContactListenerTest.cpp
 	${SAMPLES_ROOT}/Tests/SoftBody/SoftBodyContactListenerTest.h
 	${SAMPLES_ROOT}/Tests/SoftBody/SoftBodyCustomUpdateTest.cpp

+ 3 - 0
Samples/SamplesApp.cpp

@@ -325,6 +325,7 @@ JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SoftBodyVertexRadiusTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SoftBodyContactListenerTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SoftBodyCustomUpdateTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SoftBodyLRAConstraintTest)
+JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SoftBodyBendConstraintTest)
 JPH_DECLARE_RTTI_FOR_FACTORY(JPH_NO_EXPORT, SoftBodySkinnedConstraintTest)
 
 static TestNameAndRTTI sSoftBodyTests[] =
@@ -342,6 +343,7 @@ static TestNameAndRTTI sSoftBodyTests[] =
 	{ "Soft Body Contact Listener",		JPH_RTTI(SoftBodyContactListenerTest) },
 	{ "Soft Body Custom Update",		JPH_RTTI(SoftBodyCustomUpdateTest) },
 	{ "Soft Body LRA Constraint",		JPH_RTTI(SoftBodyLRAConstraintTest) },
+	{ "Soft Body Bend Constraint",		JPH_RTTI(SoftBodyBendConstraintTest) },
 	{ "Soft Body Skinned Constraint",	JPH_RTTI(SoftBodySkinnedConstraintTest) }
 };
 
@@ -524,6 +526,7 @@ SamplesApp::SamplesApp()
 			mDebugUI->CreateCheckBox(drawing_options, "Draw Soft Body Vertices", mBodyDrawSettings.mDrawSoftBodyVertices, [this](UICheckBox::EState inState) { mBodyDrawSettings.mDrawSoftBodyVertices = inState == UICheckBox::STATE_CHECKED; });
 			mDebugUI->CreateCheckBox(drawing_options, "Draw Soft Body Vertex Velocities", mBodyDrawSettings.mDrawSoftBodyVertexVelocities, [this](UICheckBox::EState inState) { mBodyDrawSettings.mDrawSoftBodyVertexVelocities = inState == UICheckBox::STATE_CHECKED; });
 			mDebugUI->CreateCheckBox(drawing_options, "Draw Soft Body Edge Constraints", mBodyDrawSettings.mDrawSoftBodyEdgeConstraints, [this](UICheckBox::EState inState) { mBodyDrawSettings.mDrawSoftBodyEdgeConstraints = inState == UICheckBox::STATE_CHECKED; });
+			mDebugUI->CreateCheckBox(drawing_options, "Draw Soft Body Bend Constraints", mBodyDrawSettings.mDrawSoftBodyBendConstraints, [this](UICheckBox::EState inState) { mBodyDrawSettings.mDrawSoftBodyBendConstraints = inState == UICheckBox::STATE_CHECKED; });
 			mDebugUI->CreateCheckBox(drawing_options, "Draw Soft Body Volume Constraints", mBodyDrawSettings.mDrawSoftBodyVolumeConstraints, [this](UICheckBox::EState inState) { mBodyDrawSettings.mDrawSoftBodyVolumeConstraints = inState == UICheckBox::STATE_CHECKED; });
 			mDebugUI->CreateCheckBox(drawing_options, "Draw Soft Body Skin Constraints", mBodyDrawSettings.mDrawSoftBodySkinConstraints, [this](UICheckBox::EState inState) { mBodyDrawSettings.mDrawSoftBodySkinConstraints = inState == UICheckBox::STATE_CHECKED; });
 			mDebugUI->CreateCheckBox(drawing_options, "Draw Soft Body LRA Constraints", mBodyDrawSettings.mDrawSoftBodyLRAConstraints, [this](UICheckBox::EState inState) { mBodyDrawSettings.mDrawSoftBodyLRAConstraints = inState == UICheckBox::STATE_CHECKED; });

+ 43 - 0
Samples/Tests/SoftBody/SoftBodyBendConstraintTest.cpp

@@ -0,0 +1,43 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Tests/SoftBody/SoftBodyBendConstraintTest.h>
+#include <Jolt/Physics/SoftBody/SoftBodyCreationSettings.h>
+#include <Utils/SoftBodyCreator.h>
+#include <Layers.h>
+
+JPH_IMPLEMENT_RTTI_VIRTUAL(SoftBodyBendConstraintTest)
+{
+	JPH_ADD_BASE_CLASS(SoftBodyBendConstraintTest, Test)
+}
+
+void SoftBodyBendConstraintTest::Initialize()
+{
+	CreateFloor();
+
+	auto inv_mass = [](uint, uint inZ) { return inZ < 2? 0.0f : 1.0f; };
+
+	{
+		// Cloth without bend constraints
+		Ref<SoftBodySharedSettings> cloth_settings = SoftBodyCreator::CreateCloth(cNumVerticesX, cNumVerticesZ, cVertexSpacing, inv_mass, SoftBodySharedSettings::EBendType::None);
+		SoftBodyCreationSettings cloth(cloth_settings, RVec3(-5.0f, 5.0f, 0), Quat::sIdentity(), Layers::MOVING);
+		mBodyInterface->CreateAndAddSoftBody(cloth, EActivation::Activate);
+	}
+
+	{
+		// Cloth with edge bend constraints
+		Ref<SoftBodySharedSettings> cloth_settings = SoftBodyCreator::CreateCloth(cNumVerticesX, cNumVerticesZ, cVertexSpacing, inv_mass, SoftBodySharedSettings::EBendType::Distance);
+		SoftBodyCreationSettings cloth(cloth_settings, RVec3(0.0f, 5.0f, 0), Quat::sIdentity(), Layers::MOVING);
+		mBodyInterface->CreateAndAddSoftBody(cloth, EActivation::Activate);
+	}
+
+	{
+		// Cloth with isometric bend constraints
+		Ref<SoftBodySharedSettings> cloth_settings = SoftBodyCreator::CreateCloth(cNumVerticesX, cNumVerticesZ, cVertexSpacing, inv_mass, SoftBodySharedSettings::EBendType::Dihedral);
+		SoftBodyCreationSettings cloth(cloth_settings, RVec3(5.0f, 5.0f, 0), Quat::sIdentity(), Layers::MOVING);
+		mBodyInterface->CreateAndAddSoftBody(cloth, EActivation::Activate);
+	}
+}

+ 23 - 0
Samples/Tests/SoftBody/SoftBodyBendConstraintTest.h

@@ -0,0 +1,23 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Tests/Test.h>
+
+// This test shows the effect of bend constraints in a soft body.
+class SoftBodyBendConstraintTest : public Test
+{
+public:
+	JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, SoftBodyBendConstraintTest)
+
+	// See: Test
+	virtual void			Initialize() override;
+
+private:
+	// Size and spacing of the cloth
+	static constexpr int	cNumVerticesX = 10;
+	static constexpr int	cNumVerticesZ = 10;
+	static constexpr float	cVertexSpacing = 0.5f;
+};

+ 5 - 3
Samples/Utils/SoftBodyCreator.cpp

@@ -8,7 +8,7 @@
 
 namespace SoftBodyCreator {
 
-Ref<SoftBodySharedSettings> CreateCloth(uint inGridSizeX, uint inGridSizeZ, float inGridSpacing, const function<float(uint, uint)> &inVertexGetInvMass)
+Ref<SoftBodySharedSettings> CreateCloth(uint inGridSizeX, uint inGridSizeZ, float inGridSpacing, const function<float(uint, uint)> &inVertexGetInvMass, SoftBodySharedSettings::EBendType inBendType, const SoftBodySharedSettings::VertexAttributes &inVertexAttributes)
 {
 	const float cOffsetX = -0.5f * inGridSpacing * (inGridSizeX - 1);
 	const float cOffsetZ = -0.5f * inGridSpacing * (inGridSizeZ - 1);
@@ -46,7 +46,7 @@ Ref<SoftBodySharedSettings> CreateCloth(uint inGridSizeX, uint inGridSizeZ, floa
 		}
 
 	// Create edges
-	settings->CreateEdges(0.00001f);
+	settings->CreateConstraints(&inVertexAttributes, 1, inBendType);
 
 	// Optimize the settings
 	settings->Optimize();
@@ -267,7 +267,9 @@ Ref<SoftBodySharedSettings> CreateSphere(float inRadius, uint inNumTheta, uint i
 	}
 
 	// Create edges
-	settings->CreateEdges(0.0001f);
+	SoftBodySharedSettings::VertexAttributes va;
+	va.mCompliance = va.mShearCompliance = 1.0e-4f;
+	settings->CreateConstraints(&va, 1);
 
 	// Optimize the settings
 	settings->Optimize();

+ 1 - 1
Samples/Utils/SoftBodyCreator.h

@@ -13,7 +13,7 @@ namespace SoftBodyCreator
 	/// @param inGridSizeZ Number of points along the Z axis
 	/// @param inGridSpacing Distance between points
 	/// @param inVertexGetInvMass Function that determines the inverse mass of each vertex
-	Ref<SoftBodySharedSettings>	CreateCloth(uint inGridSizeX = 30, uint inGridSizeZ = 30, float inGridSpacing = 0.75f, const function<float(uint, uint)> &inVertexGetInvMass = [](uint, uint) { return 1.0f; });
+	Ref<SoftBodySharedSettings>	CreateCloth(uint inGridSizeX = 30, uint inGridSizeZ = 30, float inGridSpacing = 0.75f, const function<float(uint, uint)> &inVertexGetInvMass = [](uint, uint) { return 1.0f; }, SoftBodySharedSettings::EBendType inBendType = SoftBodySharedSettings::EBendType::None, const SoftBodySharedSettings::VertexAttributes &inVertexAttributes = { 1.0e-5f, 1.0e-5f, 1.0e-5f });
 
 	/// Same as above but fixates the corners of the cloth
 	Ref<SoftBodySharedSettings>	CreateClothWithFixatedCorners(uint inGridSizeX = 30, uint inGridSizeZ = 30, float inGridSpacing = 0.75f);