So lets say we have 3 CFrames, lets call them… A, B, and C. These represent a triangle in 3D space, B is the tip, where the triangle is actually pointing towards. Now, I’m not too “math savvy”, but I need a CFrame which is placed in the middle of these 3 points, and faces along the direction it’s pointing, in all directions (Yaw, Pitch, and Roll). Of course the CFrame.new(origin, point towards) implementation won’t work, due to multiple issues, and it not getting the result I’m looking for. Here’s an example:

The mean, rather. The line segment from the midpoint of AC to B will pass through this point, it’s the Centroid on that page of triangle centers, so you could use that point. But why average 3 points when you only need to average 2. The vector you want for your CFrame LookVector is simply: B-(A+C)/2. The surface normal of this triangle can be gotten from the cross product of any of the sides with each other, or with this new look vector, since they’re all in a plane. This normal could be used for the UpVector of your CFrame, or the -UpVector, or +/-RightVector, or none of these depending on how you want that CFrame aligned with the triangle.

There is a CFrame constructor that takes all 12 entries of the CFrame. You could use this to construct the CFrame from the LookVector, the normal, and the cross product of these two. Check out the robloxdev CFrame page, it shows you which arguments to the constructor are which. EgoMoose’s CFrame tutorial page has a lot more info. Which vector’s values you pass as the look, right, and upvector values depend on how you need that CFrame oriented relative to the triangle to solve your problem.

Example, assumes a, b, c are all Vector3 that could have come from CFrame.p values. Example not tested, I’m just going to type this from memory:

local midpoint = 0.5*(a+c)
local look = (b - midpoint).Unit
local up = (c-a):Cross(b-a).Unit
local right = look:Cross(up)
local cf = CFrame.new(midpoint.X, midpoint.Y, midpoint.Z, right.X, up.X, -look.X, right.Y, up.Y, -look.Y, right.Z, up.Z, -look.Z)

In this particular case, the CFrame will be positioned halfway between A and C, pointing with the look vector at B, and the up vector will be normal to the triangle in the way that is naturally “up” in your first illustration. Roblox look vector is in the -Z direction, that’s why all the look entries are negated. I could just have easily computed -look directly as (mid-b).Unit. If you want the CFrame positioned at the centroid, then just use midpoint = (a+b+c)/3.

Lastly, might be worth double-checking that the centroid is the triangle center definition that is correct for your use case. The incenter is also common, and what a lot of people will solve for if you just ask them to find the center of a triangle. The difference is that the look vector of your CFrame would bisect the angle at point B rather than the side AC.

Give this a try. Again, using a, b, and c and positions.

local rollCFrame = CFrame.new((a+c)*.5, c)
local localCFrame = CFrame.new(Vector3.new(0,0,0), rollCFrame:pointToObjectSpace(b))
local finalCFrame = rollCFrame * localCFrame

If you do something like CFrame.new((a+c)/2, b), you get a valid CFrame that points in the direction you’ve specified, but it will be one with a RightVector in the world XZ plane most of the time. This is often what you want if you have either a camera or character you want to point a specific way while staying “upright” where up is the +Y world direction. Where things get a bit dodgy is if you have a triangle that is very skinny, and the direction vector ends up pointing almost but not quite in the +Y or -Y world direction. Then you’ll get something very difficult to predict. For example, if the points A, B, C are (0,0,0), (0,8,0), and (-1,16,-1), what would you get? Two solutions exist of course with a right vector in the XZ plane, and one of these has an up vector with a positive Y coordinate. You should get that solution, but alas… that constructor gives you something a lot less consistent and a lot less useful (try it). This numerical instability is the source of a lot of pain and bugs.

This just doubles the problem by using that constructor twice. For certain special cases you’ll get a “world upright” CFrame, and for others a CFrame that’s oriented to the triangle, but these are just coincidences, or more accurately points where the results of this code intersect with one of the useful solutions. For just any arbitrary triangle in 3D space the result of this will not be either of these useful orientations.