The Beauty of Verlet Integration
How I created a smooth Ragdoll, with nothing but Guis
Overview
One of fanciest if not the most optimal way of simulating physics in physics engines, computer graphics and games is the Verlet Integration. One of the famous 2D Physics engine that goes by the name of Box2D uses this integration. Another physics engine that run in the browser, for example matter.js also uses this magical way of smooth physics simulations.
If you want to learn more about verlet integration, want to make smooth physics simulations using guis or are just curious, then this post is an engrossing adventure for you.
Now to the jewel of this post. What is verlet integration? Verlet is a set of mathematical algorithms used to integrate physics in terms of Newtonās laws of motion. It is mostly used in Computer Graphics and Game Development. It lies around points connected with line segment and realistic motion. Weāll look into the algorithms in a moment, but first Iād like to share why I am making this topic. After all, Roblox isnāt made for 2D games that consist of only UI elements, and thatās the reason I have written this for you, to give you sort of an idea and an overlook of how you can simulate physics in roblox using merely guis! @EgoMoose has written a beautiful article for Verlet Integration inclined towards 3D objects, so be sure to check that out if you are more into 3D Verlet Integration than 2D.
I will be covering the fundamentals as well as how I created this ragdoll using only guis and verlet integration:
Creating Attachments - Points
Verlet Integration can be used to create cloth simulations, ragdolls, ropes, swings, rigid bodies and other such physics simulations.
Before diving into how it works and how to code something like this, you must have knowledge about Vectors, Vector math operations, knowledge about basic geometrical and physical terms/quantities.
To begin with Verlet Integration, it is important to know how it works.
Verlet integration works on the basis of points that act as attachments and joints for different line segments. These line segments connect different points to keep uniform distance between two points even when in motion. The beauty of verlet is such that these points and segments can be used to create realistic cloth, rope, ragdolls and other physics simulations.
We can use verlet integration to create custom physics engines.
Now onto the fundamantals. How do we create these points and line segments? How do we connect them?
The answer to all these questions are fairly easy to understand and to apply to your code. Note that weāll be using guis for the rest of this tutorial. Using simple object oriented programming, we can create a Point class.
local Point = {}
Point.__index = Point
function Point.new(posX, posY, visible)
-- creating the UI element for debugging/visualization processes.
local ellipse = Instance.new("Frame")
ellipse.Size = UDim2.new(0, 5, 0, 5) -- circle of diameter 5
ellipse.Position = UDim2.new(0, posX, 0, posY)
ellipse.BackgoundColor3 = Color3.new(0, 1, 0) -- green
ellipse.Visible = visible
ellipse.Parent = canvas -- canvas aka frame that will hold this ellipse
-- metatable
local self = setmetatable({
frame = ellipse, -- our UI element
oldPos = Vector2.new(posX, posY), -- previous position (Starts with initial position)
pos = Vector2.new(posX, posY), -- current position
forces = Vector2.new(0, 0), -- forces to be applied to the UI element.
stiff = false, -- should it not move?
}, Point)
return self
end
function Point:ApplyForce(force)
self.forces += force
end
return Point
Previous position and current position are two important vector quantities that we will be using in this tutorial. Previous position would store the position right before movement forces are applied to the point, and the current position changes as we move the point.
We wonāt be moving line segments, but only points. Naturally, line segments joining these points will appear as if the segments are moving.
The ApplyForce function is used to apply forces to the UI element, mainly the gravitational force.
Now to simulate this point to be attracted by the ground to simulate a gravitational pull and to apply frictional forces, we can create another function.
function Point:Simulate()
if not self.stiff then
local gravity = Vector2.new(0, .1)
self:ApplyForce(gravity) -- apply gravitional force
local velocity = self.pos
velocity -= self.oldPos
velocity += self.forces
local friction = .99
velocity *= friction -- apply frictional force to the velocity
self.oldPos = self.pos -- set old position before moving the ui element
self.pos += velocity -- finally moving the ui element
self.forces *= 0 -- setting forces back to 0 to prevent infinite movement
else
self.oldPos = self.pos
end
end
We have a simulation function, but when running what we have till now, youāll notice the point just falls down infinitely.
To prevent this from happening, weāll create another function that checks if the point goes past the borders of the screen, if it does, it brings the point back inside and applies a bounce force to it to simulate smooth collisions
local height = workspace.CurrentCamera.ViewportSize.Y
local width = workspace.CurrentCamera.ViewportSize.X
local bounce = .8 -- bounce force damp value between 0 and 1
function Point:KeepInCanvas()
local vx = self.pos.x - self.oldPos.x; -- velocity.x
local vy = self.pos.y - self.oldPos.y; -- velocity.y
if self.pos.y > height then -- if it crosses the bottom edge
self.pos = Vector2.new(self.pos.x, height) -- adjust position
self.oldPos = Vector2.new(self.oldPos.x, self.pos.y + vy * bounce) -- apply bounce force
elseif self.pos.y < 0 then -- if it crosses the top edge
self.pos = Vector2.new(self.pos.x, 0)
self.oldPos = Vector2.new(self.oldPos.x, self.pos.y - vy * bounce)
end
if self.pos.x < 0 then -- if it crosses the left edge
self.pos = Vector2.new(0, self.pos.y)
self.oldPos = Vector2.new(self.pos.x + vx * bounce, self.oldPos.y)
elseif self.pos.x > width then -- if it crosses the right edge
self.pos = Vector2.new(width, self.pos.y)
self.oldPos = Vector2.new(self.pos.x - vx * bounce, self.oldPos.y)
end
end
Cool we have everything setup. Now to render this point every frame, weāll run a function every RenderStepped, which does nothing but sets the UI elementās position to self.pos
and run the KeepInCanvas function!
function Point:Draw()
self.frame.Position = UDim2.new(0, self.pos.x, 0, self.pos.y) -- this is optional and only for debugging purposes.
self:KeepInCanvas()
end
Note, that rendering points isnāt needed at all as it may give rise to performance issues, I like to use it for debugging processes only.
Creating Segments
We just created points. Soā¦ Why not connect these points with segments to form polygons? To do this, weāll create another class! The constructor takes in a few arguments. The first 2 being point 1 and point 2. The segment will be connected to these 2 points
local Segment = {}
Segment.__index = Segment
function Segment.new(p1, p2, visible, th)
local self = setmetatable({
frame = line(0, 0, 0, 0, canvas, th), -- line segment's UI element
point1 = p1,
point2 = p2,
length = (p2.pos - p1.pos).magnitude, -- minimum length possible
visible = visible,
th = th or 4 -- thickness
}, Segment)
return self
end
return Segment
You might have noticed the line()
function that I wrote there. That function will be used to render the line segment on the screen.
The math behind it, is fairly simple. Just find the angle between the two points, then, rotate and position the frame correctly!
local function draw(hyp, origin, thickness, parent, l)
local line = l or Instance.new("Frame")
line.Name = "line"
line.AnchorPoint = Vector2.new(.5, .5)
line.Size = UDim2.new(0, hyp, 0, thickness or 1)
line.BackgroundColor3 = Color3.new(1,1,1)
line.BorderSizePixel = 0
line.Position = UDim2.fromOffset(origin.x, origin.y)
line.ZIndex = 1
line.Parent = parent
return line
end
function line(originx, originy, endpointx, endpointy, parent, thickness, lineToUpdate)
local origin = {
x = originx,
y = originy
}
local endpoint = {
x = endpointx,
y = endpointy
}
local adj = (Vector2.new(endpoint.x, origin.y) - Vector2.new(origin.x, origin.y)).magnitude
local opp = (Vector2.new(endpoint.x, origin.y) - Vector2.new(endpoint.x, endpoint.y)).magnitude
local hyp = math.sqrt(adj^2 + opp^2)
local line = lineToUpdate and draw(hyp, origin, thickness, parent, lineToUpdate) or draw(hyp, origin, thickness, parent)
local mid = Vector2.new((origin.x + endpoint.x)/2, (origin.y + endpoint.y)/2)
local theta = math.atan2(origin.y - endpoint.y, origin.x - endpoint.x)
theta /= math.pi
theta *= 180
line.Position = UDim2.fromOffset(mid.x, mid.y)
line.Rotation = theta
return line
end
Weāll mostly use this in our code. origin and endpoint parameters will be the two points which the line segment will be connected to. The lineToUpdate parameter is the frame, and we are just updating the position and rotation of the frame. This method is basically reusing frames that we initialized and updating them accordingly.
To Render this UI element every frame, we do the same thing we did for Points, but use the line() function!
function Segment:Draw()
if self.visible then -- if visible, create a line!
line(self.point1.pos.x, self.point1.pos.y, self.point2.pos.x, self.point2.pos.y, script.Parent.Parent.Canvas, self.th, self.frame)
end
end
This function reuses self.frame
and updates the UI element!
But, as our points move, we should keep a constant distance between the points! So weāll create another function that does this job.
function Segment:Simulate()
local currentLength = (self.point2.pos - self.point1.pos).magnitude -- length of the segment
local lengthDifference = self.length - currentLength -- difference of minimum length and currentlength
local offsetPercent = (lengthDifference / currentLength) / 2 -- offset
local direction = self.point2.pos
direction -= self.point1.pos
direction *= offsetPercent -- direction to pull the point back to maintain constant length
if not self.point1.stiff then
self.point1.pos -= direction -- updating point's position
end
if not self.point2.stiff then
self.point2.pos += direction -- updating point's position
end
end
We have our attachments and segments ready to render now! So lets try making a Box that moves according to your mouse location and bounces off the edges!
Making a bouncy box
Weāll now use our Points and Segments to create a Box that clings to your cursor when you hold it and bounces off the edges! In this part of the tutorial, weāll create something the following:
This is nothing but 4 points connected with line segments
Sooo, lets start with initialization of how our box would look! Here, weāll store the segments and points in a table. Weāll keep the points visible for visualization.
local Segment = require(path.to.module)
local Point = require(path.to.module)
local points = {}
local segments = {}
function Setup()
-- four corners of the box
local topLeft = Point.new(300, 300, true)
local topRight = Point.new(350, 300, true)
local bottomLeft = Point.new(300, 350, true)
local bottomRight = Point.new(350, 350, true)
points = { topLeft, topRight, bottomLeft, bottomRight }
-- segments of the box
local leftEdge = Segment.new(topLeft, bottomLeft, true)
local topEdge = Segment.new(topLeft, topRight, true)
local rightEdge = Segment.new(topRight, bottomRight, true)
local bottomEdge = Segment.new(bottomLeft, bottomRight, true)
end
Setup()
When, weāll make these points movable, youāll notice the box will just collapse. To prevent this from happening, weāll join the Top left corner with the bottom right. This line segment is a āSupport Beamā and gives the box durability to not collapse. You can make it invisible, but for debugging processes weāll render it.
local support = Segment.new(topLeft, bottomRight, true)
Lets run this, Andā¦
Hmm? Thereās nothing on the screen? Youāll notice that we never really rendered the elements using :Draw(), so the points and segments remain at their initial position which in our case was 0, 0!
So lets render these points and lines every frame!
function Render()
for timeStep = 1, 6 do
for _, point in ipairs(points) do
point:Simulate() -- simulation
end
end
for _, segment in ipairs(segments) do
segment:Simulate() -- simulate segments
end
for _, point in ipairs(points) do
point:Draw() -- rendering on screen
end
for _, segment in ipairs(segments) do
segment:Draw() -- rendering on screen
end
end
We use a TimeStep loop here, timesteps are used to render something multiple times in a given time frame. Here we simulate the position of the segment 6 times in a time frame!
Now, lets run this!
Amazing! The points are visible in green, you can also see the support beam. The points uniformly fall down due to our segments pulling them to keep constant distance between the points!
Selecting points and moving them around on the screen is fairly simple using UserInputService, so I wonāt be covering that. When you implement selection of points, youāll notice a smooth and realistic movement of the box on the screen, with the act of gravitational forces on the points and the beauty of Verlet integration you get this result:
If it feels to bobbly, you can add another support beam which connects the topright corner and bottom left corner of the box:
Looks great to me! You can play around with points and segments to create other kinds of quadrilaterals and even polygons with more or less than 4 sides!
How I made the Ragdoll
I made this post to explain to you all, how I made the ragdoll I showcased towards the beginning of the tutorial along with the fundamentals of Verlet Integration. So far, you must have got the gist of how magical Verlet Integration can be.
Making a ragdoll was fairly simple. I used the same Points and Segments I used to make the box above! But with just a few more points and segments to make it look like a Human Ragdoll!
Firstly, something I forgot to explain above, How do OBB Collisions in Verlet Integration work? āI didnāt see you code them?ā, this is what many of you would say. But the thing is, Verlet Integration itself handles them for me! And technically fakes OBB Collisions!
Hereās how:
When a point collides with the edges of the screen, a bounce force is applied to it. Which naturally causes a change in its position and the line segmentās position! Hence, faking OBB Collisions when rendering!
Now, to the next point. How exactly did I connect the points to make it look like a human? Hereās a rig of all the joints (points) and bones (segments)!
Yeah, I know they are the most perfectly straight lines ever seen.
Thatās just the basic Idea of how the Rig of the Ragdoll looks behind the scenes with the Support Beams. Its a combination of quadrilaterals and free rope like limbs!
Then, using UserInputService I make the points selectable and draggable! Verlet Integration is super cool and flexible that it does the job of realistic movement of the ragdoll (which isnāt an active ragdoll) so smoothly with the basic algorithm that I discussed above in the post!!
Hereās the ragdoll in action:
If you would like to take a glimpse at the code behind this ragdoll, feel free to view it and edit it! It can be found in the placefile below:
Verlet Integration - Ragdoll.rbxl (35.1 KB)
Conclusion
Not only are these ragdolls fun to control and play around but also fun to make! I would love to see your own versions/edits of the ragdolls! If you do make one or make something different with Verlet Integration, be sure to show it to others in the replies below! I hope this post about Verlet Integration helped you out in some way. Iāll update this topics with a few more tweaks and information. If you would like to read more about Verlet Integration and the deep mathematical equations, donāt forget to check out this wikipedia page!
If you want to just have fun playing with the ragdoll, you can do so here:
Thatās it from me today,
Thanks!
Update Log
- In the tutorial, I mentioned that the length parameter of the Segment is the minimum possible length of the Segment, thatās incorrect, as in the code we try to keep the segmentās length constant. So itāll be referred to as the Maximum length of the segment.
- TimeStep has to be used to render the points rather than the segments.