Triangulating polygons using the Ear Clipping algorithm

Hello roblox math lovers !!
Today I am gonna talk about the ear clipping algorithm that can be used to triangulate a polygon. This can be useful in case you want to make a irregular polygon; with that algorithm you can only have to give the vertices of the polygon and it will be filled automatically !

Before showing how to code the algorithm I would like to explain some important math concepts that are needed. So, let’s get into it.

What are you gonna learn in this tutorial ?
  • Some basic stuff about the vectors
  • What is the cross product and how it works
  • Polygon triangulating with the ear clipping algorithm

So, what is a vector ?

It would be finicky start talking about calculus with vectors without explaining some basic stuff about them.

A vector is a mathematical entity which is represented by a straight line and an arrow indicating the way.

vector

In order to create a vector you need two points like in the example where there is the point O (0,0) and the point A (2,3). Therefore you have to subtract to the X component of A, the X component of O, and then the same for the Y component → (2 - 0, 3 - 0) = (2,3)

Now, that you know how to create a vector, let’s talk about the module. The module of a vector is the distance between the two points. It can be calculated by doing the square root of the addition of the vector components squared → √(x^2 + y^2) or translated in roblox code:

local vector = Vector2.new(2,3)

print(math.sqrt((vector.X^2) + (vector.Y^2)))
print(vector.Magnitude) -- using .Magnitude will output the same as doing the method explained above

The module can be represented with | the name of the vector | → | OA |

Let’s start doing more complicated calculus with vectors

Before explaining what is the ear clipping and how to code it I need to talk about the cross product. The cross product is a way to multiply two vectors and the result will be a new vector in the third dimension. Yes, now we are talking about 3D vectors but don’t get scare, it is very easy to understand if you pay attention.

image

The module of the vector resulted is equal to the area of the parallelogram made by the two vectors

The formula for the cross product is the following: a × b = | a | | b | sin(θ). Let’s see what this mean: a and b are two vectors, | a | and | b | are they respective module, sin is a trigonometric formula and θ represents any angle.

We can also get the new vector using this easier way:

  • c.X = a.Y x b.Z − a.Z x b.Y – the component x of the new vector
  • c.Y = a.Z x b.X − a.X x b.Z – the component y of the new vector
  • c.Z = a.X x b.Y − a.Y x b.X – the component z of the new vector

You can check this link where you can see more graphically how the cross product works.

What the hell is the ear clipping ?

A lot of people have never heard about the ear clipping, and I don’t gonna lie, a week ago, me neither. The ear clipping method is one of the methods for triangulating polygons, it is not the fastest but it is easy to implement on code so I decided to do it. (Something important to say is that this algorithm doesn’t work with polygons that have holes).

This triangulating method is based on the Two Ears Theorem which says that any polygon with more than 3 vertices will have to ears/triangles. The ear clipping consists on finding one ear and removing it from the polygon ( this results onto a new polygon ) and repeat the process until there is only one triangle left.

And, how we can find the ear ?
Easy, first we need to find a vertex and make a triangle joining the next vertex with the previous:


Then we need to check the angle “θ”. Here is where the cross product comes into play. Before making the calculus we need to define the vectors and calculate their own module:

-- Is important to call A the vector that is made with the vertex and his previous one
local vectorA = Vector2.new(a.Position.X - b.Position.X, a.Position.Z - b.Position.Z)	
local vectorB = Vector2.new(a.Position.X - c.Position.X, a.Position.Z - c.Position.Z)
local moduleA = math.sqrt((vectorA.X)^2+(vectorA.Y)^2)
local moduleB = math.sqrt((vectorB.X)^2+(vectorB.Y)^2)

Just after I calculate only the Y component of the cross vector cause the other two will be zero.

-- c.Y = a.X x b.Z − a.Z x b.X
local CrossProductY = (vectorA.X * vectorB.Y) - (vectorA.Y * vectorB.X)

( here you can see that in the comment I put b.Z and a.Z and then in the variable I put .Y, that’s because in the Vector2 the Y component is the same as the Z component in Vector3 )

So, now that we have calculated the cross product of the two vectors we can get the angle and check if is lower to 180 degrees ( if is higher it’s impossible to make a triangle!)

-- a × b = |a| |b| sin(θ) 
-- sin(θ) = (a x b)/(|a| |b|)
local sin = CrossProductY/(moduleA*moduleB)
local angle = math.deg(math.asin(sin)) -- math.asin let's you get the angle of a given sin

Now that we have checked we can make a triangle we have to check if there is any other vertex inside the triangle. For that I used the cross product as well. Let me show you how I did it.


First, I make a vector from to point to every vertex of the triangle. Then, I check the direction of the angle ( I make the direction from one vector to the next using the shortest angle ). If all the directions go in the same way means that the point is into the triangle as we can se in the image, if not, we can verify that the point is out.

For that, I make the cross product of every vector with the next one and check if it is positive or negative:

function checkPointIntoTriangle(point: Instance, a: Instance, b:Instance, c:Instance)
	local pointToa = Vector2.new(point.Position.X - a.Position.X, point.Position.Z - a.Position.Z)
	local pointTob = Vector2.new(point.Position.X - b.Position.X, point.Position.Z - b.Position.Z)
	local pointToc = Vector2.new(point.Position.X - c.Position.X, point.Position.Z - c.Position.Z)
	local moduleA = math.sqrt((pointToa.X)^2+(pointToa.Y)^2)
	local moduleB = math.sqrt((pointTob.X)^2+(pointTob.Y)^2)
	local moduleC = math.sqrt((pointToc.X)^2+(pointToc.Y)^2)
	-- cy = axbz − azbx
	local CrossProductY_1 = (pointToa.X * pointTob.Y) - (pointToa.Y * pointTob.X)
	local CrossProductY_2 = (pointTob.X * pointToc.Y) - (pointTob.Y * pointToc.X)
	local CrossProductY_3 = (pointToc.X * pointToa.Y) - (pointToc.Y * pointToa.X)	
	
	if CrossProductY_1 <0 or CrossProductY_2 <0 or CrossProductY_3 <0 then
		return true
	end
end

Finally, when I get all the triangles I draw it using a function created by the user EgoMoose

Postscript: I made a folder with all the vertices before making the script. I recommend you to make the same an call it as the same way I did, unless it wont work
image

Here is the complete script:

local vertices = game.Workspace:WaitForChild("Vertices")

local vertexlist = vertices:GetChildren()
local vertexfound = false

local wedge = Instance.new("WedgePart");
wedge.Anchored = true;
wedge.TopSurface = Enum.SurfaceType.Smooth;
wedge.BottomSurface = Enum.SurfaceType.Smooth;

function checkPointIntoTriangle(point: Instance, a: Instance, b:Instance, c:Instance)
	local pointToa = Vector2.new(point.Position.X - a.Position.X, point.Position.Z - a.Position.Z)
	local pointTob = Vector2.new(point.Position.X - b.Position.X, point.Position.Z - b.Position.Z)
	local pointToc = Vector2.new(point.Position.X - c.Position.X, point.Position.Z - c.Position.Z)
	local moduleA = math.sqrt((pointToa.X)^2+(pointToa.Y)^2)
	local moduleB = math.sqrt((pointTob.X)^2+(pointTob.Y)^2)
	local moduleC = math.sqrt((pointToc.X)^2+(pointToc.Y)^2)
	-- cy = axbz − azbx
	local CrossProductY_1 = (pointToa.X * pointTob.Y) - (pointToa.Y * pointTob.X)
	local CrossProductY_2 = (pointTob.X * pointToc.Y) - (pointTob.Y * pointToc.X)
	local CrossProductY_3 = (pointToc.X * pointToa.Y) - (pointToc.Y * pointToa.X)	
	
	if CrossProductY_1 <0 or CrossProductY_2 <0 or CrossProductY_3 <0 then
		return true
	end
end

function checkVertex(a: Instance,b: Instance, c:Instance)
	
	local canMakeTriangle = true
	
	local vectorA = Vector2.new(a.Position.X - b.Position.X, a.Position.Z - b.Position.Z)	
	local vectorB = Vector2.new(a.Position.X - c.Position.X, a.Position.Z - c.Position.Z)
	local moduleA = math.sqrt((vectorA.X)^2+(vectorA.Y)^2)
	local moduleB = math.sqrt((vectorB.X)^2+(vectorB.Y)^2)
	-- cy = axbz − azbx
	local CrossProductY = (vectorA.X * vectorB.Y) - (vectorA.Y * vectorB.X)
	-- a × b = |a| |b| sin(θ) 
	-- sin(θ) = (a x b)/(|a| |b|)
	local sin = CrossProductY/(moduleA*moduleB)
	local angle = math.deg(math.asin(sin))	

	if angle > 0 then -- if the angle is higher than 180 degrees the variable angle will be a number under of 0 because the cross product will be negative
		for i, v in pairs(vertexlist) do
			if v ~= a and v ~= b and v ~= c then
				if not checkPointIntoTriangle(v, a, b, c) then
					canMakeTriangle = false
				end
			end			
		end
		if canMakeTriangle then
			return true
		end
	end
end

local function draw3dTriangle(a, b, c, parent, w1, w2)
	local ab, ac, bc = b - a, c - a, c - b;
	local abd, acd, bcd = ab:Dot(ab), ac:Dot(ac), bc:Dot(bc);

	if (abd > acd and abd > bcd) then
		c, a = a, c;
	elseif (acd > bcd and acd > abd) then
		a, b = b, a;
	end

	ab, ac, bc = b - a, c - a, c - b;

	local right = ac:Cross(ab).unit;
	local up = bc:Cross(right).unit;
	local back = bc.unit;

	local height = math.abs(ab:Dot(up));
	
	w1 = w1 or wedge:Clone();
	w1.Size = Vector3.new(0, height, math.abs(ab:Dot(back)));
	w1.CFrame = CFrame.fromMatrix((a + b)/2, right, up, back);
	w1.Parent = parent;
	w1.BrickColor = BrickColor.random()

	w2 = w2 or wedge:Clone();
	w2.Size = Vector3.new(0, height, math.abs(ac:Dot(back)));
	w2.CFrame = CFrame.fromMatrix((a + c)/2, -right, up, -back);
	w2.Parent = parent;
	w2.BrickColor = w1.BrickColor

	return w1, w2;
end


while task.wait() do
	for i,v in pairs(vertexlist) do	
		if i == 1 then
			if checkVertex(v , vertexlist[i+1] , vertexlist[#vertexlist]) then				
				vertexfound = true
				draw3dTriangle(v.Position, vertexlist[i+1].Position, vertexlist[#vertexlist].Position, workspace)				
				print(v.Name..","..vertexlist[i+1].Name..","..vertexlist[#vertexlist].Name)			
				table.remove(vertexlist, i)
			end		
		elseif i == #vertexlist then
			if checkVertex(v, vertexlist[1], vertexlist[i-1]) then				
				vertexfound = true
				draw3dTriangle(v.Position, vertexlist[1].Position, vertexlist[i-1].Position, workspace)				
				print(v.Name..","..vertexlist[1].Name..","..vertexlist[i-1].Name)				
				table.remove(vertexlist, i)
			end	
		elseif i ~= 1 and i ~= #vertexlist then	
			if checkVertex(v , vertexlist[i+1] , vertexlist[i-1]) then				
				vertexfound = true
				draw3dTriangle(v.Position, vertexlist[i+1].Position, vertexlist[i-1].Position, workspace)
				print(v.Name..","..vertexlist[1].Name..","..vertexlist[i-1].Name)			
				table.remove(vertexlist, i)
			end		
		end	
		if vertexfound then
			vertexfound = false
			break
		end
	end
	if #vertexlist <= 1 then
		break
	end
end

If you want to copy the original place you can !!

Thanks for taking your time to reading this tutorial and I would like to express special thanks to @Kriko_YT that told me about the ear clipping.

If you enjoyed the tutorial please consider to leave a like and comment what other math things you want to learn.

35 Likes

Awesome tutorial, I’m not that great with math as I only hold the knowledge of basic algebra, but it was easy for me to understand what you were saying, I can definitely see people using this to create terrain and other fun stuff. :+1:

2 Likes

Nice tutorial! Now do holes! :smiley:

2 Likes

Thanks man, I would like to be at tour level one day, just now I was seeing the seminar yo did. I will try to do holes but for that I need to use other method

It’s possible to use ear clipping to do holes - you just need to introduce two edges connecting the hole to the outside of the polygon.
How you do that is up to you!

Oh I didn’t think this was even posible!! When I have time I will try to do it. Thanks for telling me !

image
Something like this…

2 Likes

image
:frowning:
can you fix it? also awsome tutorial!

1 Like

You don’t have to go to play the experience but open it in roblox studio with this button, so you can also read the script.

01d13a1c0dcc93e4fb3849e2029281e6a16caee1

1 Like

Oh I didnt saw it. Lol

1 Like

What if you make it 2 Dimensional?

It is already 2D, the thing is that when I draw the triangles I draw it in 3D. Overall, the whole algorithm is in 2D

Also I cant edit the place. Please send a copy of it.

There is a problem but I don’t know why. Try downloading the place instead of editing it

I cant download it as well!

Humm. This is something very strange, however, my friend can download it

can you send me the file here on devforum?

Ear-Clipping.rbxl (36.1 KB)

1 Like

Maybe late, but your algoritm have one flaw, which you can see, if you just flip all vertices by 180 degrees.
image

Has sense because I didn’t take into account the rotation, only the position. Thanks for telling me

1 Like