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**

- CFrame rotation matrix
- Dot product
- Cross product (Right hand rule!)
- Basic vector math

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.

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.

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).

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?

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.

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.

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;

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;