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)