Splitting Meshes/Triangles (Properly Guide)

This is renewed version of An Introductory Guide to Splitting Meshes
a more in-depth tutorial about the algorithm behind it, but more readable, less algebras and easier


First Look: Splitting Meshes/Triangles


Splitting a Triangle

Every mesh is made out of triangles, so before cutting full objects, we first need to learn how to cut a single triangle with a plane

Here’s the idea

  • A triangle has three points
  • Get the positive, negative sides of the points
  • Positive means if the point is above the plane
  • Negative means if the point is below the plane

The code below does exactly that

local function cutTriangle(trix, plane, positive)
	-- the plane point is the plane position
	-- the plane's "normal" (its top direction of the plane), Imagine the plane size is ..., 0.001, ...
	local point, normal = plane.Position, plane.CFrame.UpVector

	local dist, side = {}, {}
	
	for i = 1, 3 do
		-- get the distance of the point to the plane = dot((vertex - planePoint), planeNormal)
		-- If positive → above the plane, negative → below the plane
		dist[i] = vector.dot(trix[i] - point, normal)
		
		-- Store which side the point is on, depending on positive params of the function
 		-- I don't use >= and <= because if the plane in the exact point as the vertex,
		-- it could glitch out or something
		side[i] = if positive then dist[i] > 0 else dist[i] < 0
	end
end

Cutting Triangle Edges

Now that we know which side of the plane each vertex is on, the next step is to walk along each edge of the triangle and decide what to keep

Here’s the idea

  • A triangle has 3 edges
  • For each edge, look at its two vertices (p1 and p2)
  • If a vertex is on the correct side, we keep it
  • If the edge crosses the plane (the vertices are on opposite sides), then the plane slices through it, we compute the intersection point
  • By doing this for all 3 edges, we collect a new set of points that represent the cut

The next code would be


	local points = {}

	for i = 1, 3 do
		-- j is the next vertex in the loop (1→2, 2→3, 3→1)
		local j = i % 3 + 1

		-- p1 and p2 are the two vertices forming the current edge
		local p1, p2 = trix[i], trix[j]

		-- d1 and d2 are their distances from the plane
		local d1, d2 = dist[i], dist[j]

		-- s1 and s2 tell if p1/p2 are inside the side we want
		local s1, s2 = side[i], side[j]

		-- If this vertex is on the correct side, keep it
		if s1 then 
			points[#points + 1] = p1 
		end

		-- If the edge crosses the plane ( invalid side )
		-- calculate the intersection point and keep it
		if s1 ~= s2 then
			-- Formula: intersection = p1:Lerp(p2, d1 / (d1 - d2))
			-- This finds the exact cut point along the edge
			points[#points + 1] = p1:Lerp(p2, d1 / (d1 - d2))
		end
	end


Creating Triangles

After walking all 3 edges, we end up with a new set of points
This set represents the shape of the clipped triangle

There are only two possible outcomes

  • 3 points : Still a triangle (just reshaped)
  • 4 points : The cut turned into a quad. We can’t draw quad, Just turns them into 2 triangles instead!

The last code would be

	local result = {}

	if #points == 3 then
		-- Case 1: Already a triangle
		result[1] = { points[1], points[2], points[3] }

	elseif #points == 4 then
		-- Case 2: Quad → split into 2 triangles
		-- Fan triangulation works: (1,2,3) and (1,3,4)
		result[1] = { points[1], points[2], points[3] }
		result[2] = { points[1], points[3], points[4] }
	end

	return result

Putting It All Together

We now have all the building blocks

  1. Classify points (above/below plane)
  2. Walk edges to collect valid vertices and intersections
  3. Rebuild new triangles from the resulting points

Let’s combine everything into one clean function

local function cutTriangle(trix, plane, positive)
	local point, normal = plane.Position, plane.CFrame.UpVector

	local dist, side, points = {}, {}, {}
	for i = 1, 3 do
		dist[i] = vector.dot(trix[i] - point, normal)
		side[i] = if positive then dist[i] >= 0 else dist[i] <= 0
	end
	
	for i = 1, 3 do
		local j = i % 3 + 1
		local p1, p2 = trix[i], trix[j]
		local d1, d2 = dist[i], dist[j]
		local s1, s2 = side[i], side[j]

		if s1 then points[#points + 1] = p1 end

		if s1 ~= s2 then
			if d1 ~= d2 then -- could be NAN
				points[#points + 1] = vector.lerp(p1, p2, d1 / (d1 - d2))
			else
				points[#points + 1] = p1
			end
		end
	end
	
	if #points == 3 then
		return {points}
	elseif #points == 4 then
		return {{points[1], points[2], points[3]}, {points[1], points[3], points[4]}}
	end
	
	return {} -- no triangles
end

After all that, the final result looks promising.


You can test it with yourself or lazy to learning the tutorial, download this file to test split a triangle

You can use 3D Triangles by @EgoMoose to filling the gaps of triangles too, Example usage:

while task.wait() do
	triangleRenderer.BeginFrame()

	local trix = { workspace[1].Position, workspace[2].Position, workspace[3].Position }

	local positive = cutTriangle(trix, plane, true)
	local negative = cutTriangle(trix, plane, false)
	
	if positive then -- get points above plane
		for _, tri in positive do
			triangleRenderer.DrawTriangle(tri[1], tri[2], tri[3], Color3.new(1, 0, 0))
		end
	end
	
	if negative then -- get points below plane, like the other side of the plane
		for _, tri in negative do
			triangleRenderer.DrawTriangle(tri[1], tri[2], tri[3], Color3.new(0, 0, 1))
		end
	end

	triangleRenderer.EndFrame()
end
12 Likes

Can you use it for any meshes, like for example I get am apple mesh, can I cut it in half with this?

2 Likes

i got it working with meshes, its not perfect but its a start


MeshSplitting_V1.rbxl (69.0 KB)

Updated version with improvements and fixes
MeshSplitting_V2.rbxl (96.9 KB)

4 Likes

I saw there wasn’t any filling from the sides that were getting cut, so I added a center vertex on the rim and filled it with triangles like a pizza. But this fails if the cuts make more than one loop/circle, and doesn’t get it right all the time around 90%. There are also some intersection issues when the mesh is resized with it not generating at all on some rare sizes and orientations.

My example


MeshSplitting_V3.rbxl (81.9 KB)

The monkey
suzanne.obj (61.6 KB)

If anyone could find a way to get it 100% that would be exciting