Connecting vertices of a part

Hello! I have a simple script that draws the vertices of all parts in a character, and now I’m trying to connect them.
image
I’m not sure what the best way of doing this would be, though.
All help appreciated!

local Vertices = {
	{1, 1, -1},
	{1, -1, -1},
	{-1, -1, -1},
	{-1, 1, -1},

	{1, 1, 1},
	{1, -1, 1},
	{-1, -1, 1},
	{-1, 1, 1}
}
local function GetPartVerts(Part)
	local Size = Part.Size
	local Corners = {}
	for _, Vector in pairs(Vertices) do
		local CornerPos = (
			Part.CFrame * CFrame.new(
				Size.X/2 * Vector[1],
				Size.Y/2 * Vector[2],
				Size.Z/2 * Vector[3]
			)
		).Position
		Corners[_] = {CornerPos, Vector}
	end
	return Corners
end

local Chars = workspace.Players
for _, Char in pairs(Chars:GetChildren()) do
	for _, Limb in pairs(Char:GetChildren()) do
		if Limb:IsA("BasePart") then
			for _, Vert in pairs(GetPartVerts(Limb)) do
				local BHA = Instance.new("BoxHandleAdornment")
				BHA.SizeRelativeOffset = (Limb.Position - Vert[1])
				BHA.Size = Vector3.new(0.1, 0.1, 0.1)
				BHA.AlwaysOnTop = true
				BHA.ZIndex = 9
				BHA.Color = BrickColor.new("Light blue")
				BHA.Adornee = Limb
				BHA.Parent = script.Parent
			end
		end
	end
end

What do you mean by “connect”?

Like an outline? (characterrr limit)

Connecting all the corner vertices to one another to create a selection box-esque outline.
I’m working towards an end result that will look something like this:
image

So what is your question, exactly?

Are you trying to project these points to the screen and then calculate the outside boundary of the character on the screen?

Are you asking how to union the 2D polygons that make up the character into one?

Are you asking how to draw lines?

1 Like

Sorry if I was not clear enough, I am trying to use BoxHandleAdornments to outline the edges of a part in 3D space.

u are trying to make this?

1 Like

I am trying to make exactly that.

1 Like

I actually have a friend that have a contact with that guy. idk if he will answer it tho

1 Like

Well I’m not really looking for a finished script, so that’s ok.
I just want to know what the best way of connecting the corners/outlining the edges is.

1 Like

My idea:

  1. For each part:
    i. Computer vertices 3D positions (you did this)
    ii. Project to screen (Camera:WorldToScreenPoint)
    iii. Compute convex hull (Giftwrapping algorithm?)
  2. Perform polygon union between all convex hull polygons (math - How do I combine complex polygons? - Stack Overflow)
  3. (optional) Compute intersection with other parts, e.g. blocking parts. and only show those.

Edit: I did step 1 at least:

image

Happy to share code if you like, but it’s a bit messy.

1 Like

Could you share the code?
Thank you so much.

Sure. Added some comments. This is just a localscript in StarterPlayerScripts. It should just work.

You might wish to change the ConvexHull method out for a more efficient algorithm, I just picked the easiest to implement. But for models with just a few parts like a character it’s probably fine.

local player = game.Players.LocalPlayer
local screen = Instance.new("ScreenGui")
screen.Parent = player.PlayerGui

type Polygon = {
	[number]: Vector2,
	bounds: Rect
}

-- Calculates (twice) the area of triangle a-b-c. The area is positive if
-- the points are ordered counter-clockwise and negative otherwise.
local function SignedTriArea(a: Vector2, b: Vector2, c: Vector2)
	local ab = b - a;
	local bc = c - b;
	return ab.X * bc.Y - bc.X * ab.Y
end

-- Returns convex hull polygon from list of points.
-- Points are ordered clockwise.
-- note: modifies input s parameter (sorts by x)
-- https://en.wikipedia.org/wiki/Gift_wrapping_algorithm
local function ConvexHull(s: {Vector2}): Polygon
	local hull = {}
	
	-- sort left -> right
	table.sort(s, function(a, b) return a.X < b.X end)
	
	local minX, minY, maxX, maxY = math.huge, math.huge, -math.huge, -math.huge
	
	local pointOnHull = s[1]
	repeat
		if pointOnHull.X < minX then minX = pointOnHull.X end
		if pointOnHull.Y < minY then minY = pointOnHull.Y end
		if pointOnHull.X > maxX then maxX = pointOnHull.X end
		if pointOnHull.X > maxY then maxY = pointOnHull.Y end
		
		table.insert(hull, pointOnHull)
		
		local endpoint = s[1] -- initial endpoint for a candidate edge on the hull
		for j = 1, #s do
			if endpoint == pointOnHull or SignedTriArea(pointOnHull, endpoint, s[j]) > 0 then
				endpoint = s[j] -- found greater left turn, update endpoint
			end
		end
		pointOnHull = endpoint
	until endpoint == hull[1]
	
	-- don't use this yet, but might need it for other things
	hull.bounds = Rect.new(minX, minY, maxX, maxY)
	
	return hull
end

-- given set of clockwise-ordered polygons, perform a union
-- operation and return the list of output polygons
local function UnionPolygons(polygons: {Polygon}): {Polygon}
	-- idea: start on the left-most vertex, move clockwise until we hit an edge,
	-- then move along that instead?
	
	-- dunno this is complex: https://mathoverflow.net/a/111323
	
	-- TODO: implement this
end

local GetVertices
do
	local vertices = {
		Vector3.new(1, 1, 1),
		Vector3.new(1, 1, -1),
		Vector3.new(1, -1, 1),
		Vector3.new(1, -1, -1),
		Vector3.new(-1, 1, 1),
		Vector3.new(-1, 1, -1),
		Vector3.new(-1, -1, 1),
		Vector3.new(-1, -1, -1),
	}
	
	-- list of 2D points of part's vertices projected to screen
	-- optionally provide obj list of vertices for a 2x2x2 object,
	-- otherwise it uses the default AABB list above.
	function GetVertices(camera: Camera, part: BasePart, obj: {Vector3}?): {Vector3}
		obj = obj or vertices
		local verts = table.create(8)
		local halfSize = part.Size / 2
		for i, v in ipairs(obj) do
			local vert3d = part.CFrame:PointToWorldSpace(v * halfSize)
			verts[i] = camera:WorldToScreenPoint(vert3d)
		end
		return verts
	end
end

-- everything else here is just for drawing

local frames = {}

-- Stretches frame between two points
local function DrawLine(frame: GuiObject, a: Vector2, b: Vector2)
	local diff = b - a
	local len = diff.Magnitude
	local angle = math.atan2(diff.Y, diff.X)
	local pos = (a + b) / 2
	
	frame.Rotation = math.deg(angle)
	frame.Size = UDim2.fromOffset(len, 4)
	frame.Position = UDim2.fromOffset(pos.X, pos.Y)
end

-- Called every frame
local function Update()
	local character = player.Character
	if not character then return end
	local camera = workspace.CurrentCamera
	
	-- get list of convex hulls for each part
	local hulls = {}
	local total = 0 -- total number of vertices
	for _, part in pairs(character:GetChildren()) do
		if part:IsA("BasePart") and part.Transparency < 0.95 then
			local hull = ConvexHull(GetVertices(camera, part))
			table.insert(hulls, hull)
			total += #hull
		end
	end
	
	-- add extra guis if we need more
	for i = #frames + 1, total do
		local f = Instance.new("Frame")
		f.Size = UDim2.fromOffset(10, 10)
		f.AnchorPoint = Vector2.new(0.5, 0.5)
		f.BackgroundTransparency = 0.3
		f.Parent = screen
		frames[i] = f
	end
	
	-- remove extra guis if we have too many
	for i = #frames, total + 1, -1 do
		frames[i]:Destroy()
		frames[i] = nil
	end
	
	-- set frames' positions
	local i = 1
	for _, hull in pairs(hulls) do
		for j = 1, #hull do
			local a = hull[j]
			local b = j < #hull and hull[j + 1] or hull[1]
			DrawLine(frames[i], a, b)
			i += 1
		end
	end
end

game:GetService("RunService"):BindToRenderStep("UpdateOutline", Enum.RenderPriority.Last.Value, Update)
2 Likes

@EgoMoose has ported a library to do step 2:

1 Like