Softbody physics w/ any mesh

Softbody physics without using the built-in physics engine. It works on any mesh and is fairly robust (length and volume constraints). Here it is working on some random meshes from the toolbox:



How it works:

The softbodies are simulated using particles and constraints (which preserve lengths and volumes with some lenience). A previous softbody simulation I made only preserved lengths which made it less robust (no volume constraints meant it could collapse into a flat object and wouldn’t be able to recover). It also didn’t support arbitrary meshes.

Particle trajectory is calculated using verlet integration with

local velocity = self.position - self.previousPosition
self.previousPosition = self.position
self.position += velocity * self.damping + gravity * dt * dt / 2
-- damping value depends on if the point is colliding with something (friction) or if its not (air resistance)

and collision is resolved with

local normal = raycast.Normal
local correction = -velocity:Dot(normal) * normal * (1 + restitution)
velocity += correction

which gets rid of the projected component of velocity onto the normal of whatever its colliding with which makes its velocity tangent to the surface (not going into it in any way). Restitution (0 to 1) is just how bouncy the collision is.
image

Constraints generally take the form

constraint(point1, point2, ...) = quantity_to_be_preserved(point1, point2...) - initialQuantity

and solving them involves finding their gradients (direction to move the points in) and how much to move them based on the discrepancy / value of the constraint, the stiffness of the softbody, and the weight of each point.

For length constraints the form is

constraint(a, b) = (a - b).Magnitude - initialLength

gradient_a = (a - b).Unit
gradient_b = (b - a).Unit

or in code:

function Edge:solve(inv_stiffness, dt)
	if self.a.inv_weight + self.b.inv_weight == 0 then return end
	local displacement = self.a.position - self.b.position
	local discrepancy = self.length - displacement.Magnitude -- the constraint
	local lambda = discrepancy / (self.a.inv_weight + self.b.inv_weight + inv_stiffness / (dt ^ 2))
	local gradient = displacement.Unit
	
	self.a.position += gradient * lambda * self.a.inv_weight
	self.b.position -= gradient * lambda * self.b.inv_weight
end

and for volume constraints the form is

constraint(a, b, c, d) = ((b - a):Cross(c - a)):Dot(d - a) - initialVolume

gradient_a = (d - b):Cross(c - b)
gradient_b = (c - a):Cross(d - a)
gradient_c = (d - a):Cross(b - a)
gradient_d = (b - a):Cross(c - a)

or in code

local function volume(a, b, c, d)
	return ((b - a):Cross(c - a)):Dot(d - a)
end

function Tetrahedron:solve(inv_stiffness, dt)
	if self.a.inv_weight + self.b.inv_weight + self.c.inv_weight + self.d.inv_weight == 0 then return end
	local a, b, c, d = self.a.position, self.b.position, self.c.position, self.d.position
	local a_w, b_w, c_w, d_w = self.a.inv_weight, self.b.inv_weight, self.c.inv_weight, self.d.inv_weight
	local discrepancy = self.volume - volume(a, b, c, d) -- the constraint
	
	local a_grad = (d - b):Cross(c - b)
	local b_grad = (c - a):Cross(d - a)
	local c_grad = (d - a):Cross(b - a)
	local d_grad = (b - a):Cross(c - a)

	local lambda = discrepancy / (a_w * a_grad.Magnitude^2 + b_w * b_grad.Magnitude^2 + c_w * c_grad.Magnitude^2 + d_w * d_grad.Magnitude^2 + inv_stiffness / (dt^2))
	
	self.a.position += lambda * a_w * a_grad
	self.b.position += lambda * b_w * b_grad
	self.c.position += lambda * c_w * c_grad
	self.d.position += lambda * d_w * d_grad
end

The overall method is called XPBD softbody simulation, with the original paper and simplified explanation (same author) available here:

The main areas of potential improvements are:

  • Mesh tetrahedralization, since right now the tetrahedrons are generated by just connecting all the triangles in the mesh to the center, which means the volumes they preserve aren’t necessarily the volumes of the mesh
  • Collision, since right now collision is handled by the vertices which means the softbody won’t detect collision with small objects that fit in the gaps between points
5 Likes

I’m not able to play the videos :frowning:

1 Like


Do these work? Its the same video but the first is a direct upload and the second is an imgur.com link.

1 Like

I’m not smart enough to understand all of this, but I’m guessing we would need to simulate a plane for each face? Or would that not work or is just inefficient.

2 Likes

I’m not sure what the optimal solution here would be, but testing the surface would be a lot more accurate than testing the vertices. I don’t think it would be too inefficient, since here the bottleneck for performance is drawing the object, the actual physics has a negligible performance impact.

1 Like

The first video works, but not the second, thank you!

1 Like

I really liked this post, so I’m trying to help slowly solve the collision issue. Currently, I only have code to find the bounding box of your vertices Lol.

local function verticesBoundingBox(vertices)
	local minX, minY, minZ = math.huge, math.huge, math.huge
	local maxX, maxY, maxZ = -math.huge, -math.huge, -math.huge

	for _, vertex in ipairs(vertices) do
		local x, y, z = vertex.X, vertex.Y, vertex.Z

		if x < minX then minX = x end
		if y < minY then minY = y end
		if z < minZ then minZ = z end

		if x > maxX then maxX = x end
		if y > maxY then maxY = y end
		if z > maxZ then maxZ = z end
	end

	return Vector3.new(minX, minY, minZ), Vector3.new(maxX, maxY, maxZ)
end

My idea was to see if the bounding box interacts with a part, run some code that I haven’t thought of yet. Will be doing this later.