3D triangles

Introduction[edit]

On occasion you may find it necessary to draw triangles given three points in 3D space. This article aims to go over one method to do that using basic trigonometry and manipulation of a CFrame’s rotation matrix. The beginning of this article will share a lot in common with the article on 2D triangles. As such it is recommended reading but not mandatory.

Prerequisites

Understanding the algorithm[edit]

One thing we have to be aware of is that Roblox doesn’t have a singular default shape to create any triangle. However, what it does have are wedges which can be used as right triangles in 3D space. The good thing about that is that we can decompose any triangle into a maximum of two right triangles. All we do is find the longest edge of the triangle which we then draw a perpendicular line to it connecting to the vertex opposite to said longest edge.

2dtriangle 1.png

The math behind triangles[edit]

The main goal of our code is going to be to find how we can split any triangle into two wedges (right triangles). To start let’s focus on getting some initial information from the three points we have. We’ll start by getting the longest edge, one of the vertices connected to it, and then the other edge connected to that vertex.

2dtriangle 2.png

function drawTriangle(a, b, c, parent)
	local edges = {
		{longest = (c - b), other = (a - b), position = b};
		{longest = (a - c), other = (b - c), position = c};
		{longest = (b - a), other = (c - a), position = a};
	};
 
	table.sort(edges, function(a, b) return a.longest.magnitude > b.longest.magnitude end);
 
	local edge = edges[1];
end;


Once we have that information we can solve for one of the angles using the dot product by using the longest edge vector and the other edge vector. Since we now have an angle we can use basic trigonometry to solve for the width and height of our first right triangle. The height of our second right triangle is the same as the first so we don’t have to worry about that. The width of the second right triangle however can be found by subtracting the width of the first right triangle from longest edge’s magnitude (which is acting as our triangle’s base).

2dtriangle 3.png

function drawTriangle(a, b, c, parent)
	-- code from before...
	local theta = math.acos(edge.longest.unit:Dot(edge.other.unit)); -- angle between two vectors
	-- SOHCAHTOA
	local s1 = Vector2.new(edge.other.magnitude * math.cos(theta), edge.other.magnitude * math.sin(theta));
	local s2 = Vector2.new(edge.longest.magnitude - s1.x, s1.y);
end;


Awesome we now have the sizes of our two right triangles. The next question is how do we properly position and rotate them so they actually represent the larger triangle they’re apart of?

Manipulating the rotation matrix[edit]

Most of the time when using CFrames we rarely fill in the rotation matrix aspect unless we know exactly what we’re doing. Luckily for us we do know what we’re doing! Since a CFrame’s rotation matrix represents the right, up, and back facing directions in a rotation we know that if we can find those vectors for each wedge in our triangle we can plug them in to create a CFrame and then we’ve got our rotation.

local cf = CFrame.new() * CFrame.Angles(math.pi/4, math.pi/3, 0);
local x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22 = cf:components();
 
local position = Vector3.new(x, y, z);
local right = Vector3.new(r00, r10, r20); -- right facing direction
local up = Vector3.new(r01, r11, r21); -- up facing direction
local back = Vector3.new(r02, r12, r22); -- back facing direction
 
print(cf.lookVector, -back); -- lookVector's the front facing direction so -back == cf.lookVector


Before we even talk about how we’re going to get the right, up, and back vectors let’s first talk about how we’re going to get the position which is pretty straightforward with some simple vector addition, subtraction, etc. Even though a wedge isn’t a rectangle it’s still positioned as such. That means the position we want for each wedge is the mid-way point across each right triangle’s hypotenuse.

Center.PNG

function drawTriangle(a, b, c, parent)
	-- code from before...
	local p1 = edge.position + edge.other * 0.5; -- wedge1's position
	local p2 = edge.position + edge.longest + (edge.other - edge.longest) * 0.5; -- wedge2's position
end;


Alright, now that we have both positions we can focus entirely on getting the facing directions for the rotation matrix. The first vector we want is the surface normal of the triangle. This is the direction that the two wedge’s left or right faces will be facing, we'll call it "right". The second direction we want is what we’ll call “back” as it will represent the front and back of the two wedges. Simply put this vector is the unit vector version of the longest edge so it's very easy to get. The final direction we want we’ll call “top” because both wedges share this facing direction for their top surface.

Directions.PNG

We can get the surface normal of the triangle by crossing the longest edge vector with the other edge vector we have and then normalizing. Once we have that surface normal we can cross it with the longest edge vector and normalize again to get the “top” direction. To understand why this works you will need to apply the right hand rule.

function drawTriangle(a, b, c, parent)
	-- code from before...
	local right = edge.longest:Cross(edge.other).unit;
	local up = right:Cross(edge.longest).unit;
	local back = edge.longest.unit;
end;


Finally we just have to plug these direction into the two wedge’s CFrames remembering to take into account that because their hypotenuses are sloping away from each other some of the directions are flipped.

function drawTriangle(a, b, c, parent)
	-- code from before...
	local cf1 = CFrame.new( -- wedge1 cframe
		p1.x, p1.y, p1.z,
		-right.x, up.x, back.x,
		-right.y, up.y, back.y,
		-right.z, up.z, back.z
	);
 
	local cf2 = CFrame.new( -- wedge2 cframe
		p2.x, p2.y, p2.z,
		right.x, up.x, -back.x,
		right.y, up.y, -back.y,
		right.z, up.z, -back.z
	);
end;


Conclusion[edit]

That’s it! When you put everything together you have a way to position and properly size two wedges to fit into any triangle.

local wedge = Instance.new("WedgePart");
wedge.Anchored = true;
wedge.TopSurface = Enum.SurfaceType.Smooth;
wedge.BottomSurface = Enum.SurfaceType.Smooth;
 
function drawTriangle(a, b, c, parent)
	local edges = {
		{longest = (c - b), other = (a - b), position = b};
		{longest = (a - c), other = (b - c), position = c};
		{longest = (b - a), other = (c - a), position = a};
	};
	table.sort(edges, function(a, b) return a.longest.magnitude > b.longest.magnitude end);
	local edge = edges[1];
	-- get angle between two vectors
	local theta = math.acos(edge.longest.unit:Dot(edge.other.unit)); -- angle between two vectors
	-- SOHCAHTOA
	local s1 = Vector2.new(edge.other.magnitude * math.cos(theta), edge.other.magnitude * math.sin(theta));
	local s2 = Vector2.new(edge.longest.magnitude - s1.x, s1.y);
	-- positions
	local p1 = edge.position + edge.other * 0.5; -- wedge1's position
	local p2 = edge.position + edge.longest + (edge.other - edge.longest) * 0.5; -- wedge2's position
	-- rotation matrix facing directions
	local right = edge.longest:Cross(edge.other).unit;
	local up = right:Cross(edge.longest).unit;
	local back = edge.longest.unit;
	-- put together the cframes
	local cf1 = CFrame.new( -- wedge1 cframe
		p1.x, p1.y, p1.z,
		-right.x, up.x, back.x,
		-right.y, up.y, back.y,
		-right.z, up.z, back.z
	);
	local cf2 = CFrame.new( -- wedge2 cframe
		p2.x, p2.y, p2.z,
		right.x, up.x, -back.x,
		right.y, up.y, -back.y,
		right.z, up.z, -back.z
	);
	-- put it all together by creating the wedges
	local w1 = wedge:Clone();
	local w2 = wedge:Clone();
	w1.Size = Vector3.new(0.2, s1.y, s1.x);
	w2.Size = Vector3.new(0.2, s2.y, s2.x);
	w1.CFrame = cf1;
	w2.CFrame = cf2;
	w1.Parent = parent;
	w2.Parent = parent;
end;