I’m in the process of trying to recreate the “outlining” effect many games have on the edges of surfaces. If you’ve ever played Antichamber or similar-looking games to that, that is exactly the kind of effect I want:
I also know of someone before me who tried to create this same effect, but although they outlined the process/rules for outlining on that post (which is the exact same as what I want), they didn’t seem to ever figure out how to do it (unless they just decided not to update the post that they had)
My game is made entirely of regular block parts, with no wedges or anything, which should make it somewhat easier. I already created a way to figure out which surfaces of a part are parallel, but I have no idea how to actually outline parts. Currently, the game is static as well, but eventually, I might add dynamic objects, and I would want this effect to be updatable, so if I could get this outlining to be relatively quick, that would be cool too.
My current code, I should be able to build off of it to get this outlining effect hopefully.
local Vertices = {
{1, 1, -1}, --v1 - top front right (v2, v4, v5)
{1, -1, -1}, --v2 - bottom front right (v6)
{-1, -1, -1},--v3 - bottom front left (v2, v4, v7)
{-1, 1, -1}, --v4 - top front left (v8)
{1, 1, 1}, --v5 - top back right (v6, v8)
{1, -1, 1}, --v6 - bottom back right
{-1, -1, 1},--v7 - bottom back left (v6, v8)
{-1, 1, 1} --v8 - top back left
}
local EdgeConnections = {
{1, 2},
{1, 4},
{1, 5},
{2, 6},
{3, 2},
{3, 4},
{3, 7},
{4, 8},
{5, 6},
{5, 8},
{7, 6},
{7, 8},
}
local faces = {
Enum.NormalId.Front,
Enum.NormalId.Back,
Enum.NormalId.Left,
Enum.NormalId.Right,
Enum.NormalId.Top,
Enum.NormalId.Bottom,
}
-- Get normal of a face on a part
local function GetNormal(part, normalId)
if normalId == Enum.NormalId.Top then
return part.CFrame.UpVector
elseif normalId == Enum.NormalId.Bottom then
return -part.CFrame.UpVector
elseif normalId == Enum.NormalId.Right then
return part.CFrame.RightVector
elseif normalId == Enum.NormalId.Left then
return -part.CFrame.RightVector
elseif normalId == Enum.NormalId.Front then
return part.CFrame.LookVector
elseif normalId == Enum.NormalId.Back then
return -part.CFrame.LookVector
end
--return part.CFrame * Vector3.FromNormalId(normalId) - part.Position
end
--Get corners of a part using premade verticies table
local function GetCorners(Part)
local corners = {}
for _, Vector in pairs(Vertices) do
table.insert(corners, (Part.CFrame * CFrame.new(Part.Size.X/2 * Vector[1], Part.Size.Y/2 * Vector[2], Part.Size.Z/2 * Vector[3])).Position)
end
return corners
end
-- Gets all corner positions of a part, then uses a premade table to get the corresponding positions of edges on that part
local function GetEdges(Part)
local corners = GetCorners(Part)
local edges = {}
for _, vertexPair in ipairs(EdgeConnections) do
table.insert(edges, {corners[vertexPair[1]], corners[vertexPair[2]]})
end
return edges
end
--return the correct component of the position of a face on a part depending on what face it is. (also return the full position for debugging if needed)
local function GetFacePos(part, normal)
if normal == Vector3.FromNormalId(Enum.NormalId.Top) or normal == Vector3.FromNormalId(Enum.NormalId.Bottom) then
local pos = part.CFrame * (normal*part.Size.Y/2)
return pos.Y, pos
elseif normal == Vector3.FromNormalId(Enum.NormalId.Left) or normal == Vector3.FromNormalId(Enum.NormalId.Right) then
local pos = part.CFrame * (normal*part.Size.X/2)
return pos.X, pos
elseif normal == Vector3.FromNormalId(Enum.NormalId.Front) or normal == Vector3.FromNormalId(Enum.NormalId.Back) then
local pos = part.CFrame * (normal*part.Size.Z/2)
return pos.Z, pos
end
end
local function compareFacePos(part1Pos, part2Pos)
--if normalId == Enum.NormalId.Top or normalId == Enum.NormalId.Bottom then
-- if part1Pos.Y == part2Pos.Y then
-- return true
-- end
--elseif normalId == Enum.NormalId.Left or normalId == Enum.NormalId.Right then
-- if part1Pos.Z == part2Pos.Z then
-- return true
-- end
--elseif normalId == Enum.NormalId.Front or normalId == Enum.NormalId.Back then
-- if part1Pos.X == part2Pos.X then
-- return true
-- end
--end
--return false
if part1Pos == part2Pos then
return true
end
return false
end
--Get all parts in a table
local parts = {}
for _, obj in pairs(script.Parent:GetChildren()) do
if obj:IsA("BasePart") then
table.insert(parts, obj)
end
end
-- Store faces of each part, the normals corresponding to each face (in case part is rotated), and the edges of each part
local surfaceDescriptions = {}
for _, part in ipairs(parts) do
local normalsOfPart = {}
local facesOfPart = {}
for _, face in ipairs(faces) do
local normal = GetNormal(part, face)
table.insert(normalsOfPart, normal)
table.insert(facesOfPart, face)
end
local edgesOfPart = GetEdges(part)
surfaceDescriptions[part] = {normalsOfPart, facesOfPart, edgesOfPart}
end
for _, part1 in ipairs(parts) do
for _, part2 in ipairs(parts) do
if part1 ~= part2 then
for i = 1, 6 do
--Get the actual positions of a face on the correct axis (eg y component if we are looking at the face on the top/bottom)
local part1Pos = GetFacePos(part1, surfaceDescriptions[part1][1][i])
local part2Pos = GetFacePos(part2, surfaceDescriptions[part2][1][i])
if compareFacePos(part1Pos, part2Pos) then --Surfaces are parallel
local surfaceGUI1 = Instance.new("SurfaceGui")
surfaceGUI1.Parent = part1
surfaceGUI1.Face = surfaceDescriptions[part1][2][i]
local surfaceGUI2 = Instance.new("SurfaceGui")
surfaceGUI2.Parent = part2
surfaceGUI2.Face = surfaceDescriptions[part2][2][i]
local randomColor2 = BrickColor.random()
local frame1 = Instance.new("Frame")
frame1.Parent = surfaceGUI1
frame1.Size = UDim2.fromScale(1,1)
frame1.BackgroundColor = randomColor2
local frame2 = Instance.new("Frame")
frame2.Parent = surfaceGUI2
frame2.Size = UDim2.fromScale(1,1)
frame2.BackgroundColor = randomColor2
end
end
--local randomColor1 = BrickColor.random()
--for _, face in ipairs(faces) do
-- local part1Normal = GetNormal(part1, face)
-- local part2Normal = GetNormal(part2, face)
-- local _, part1Pos = GetFacePos(part1, part1Normal)
-- local _, part2Pos = GetFacePos(part2, part2Normal)
-- local part = Instance.new("Part", workspace)
-- part.Anchored = true
-- part.Size = Vector3.new(1.1,1.1,1.1)
-- part.BrickColor = randomColor1
-- part.Material = Enum.Material.Neon
-- part.Position = part1Pos
-- if compareFacePos(part1Pos, part2Pos, face) then
-- local surfaceGUI1 = Instance.new("SurfaceGui")
-- surfaceGUI1.Parent = part1
-- surfaceGUI1.Face = face
-- local surfaceGUI2 = Instance.new("SurfaceGui")
-- surfaceGUI2.Parent = part2
-- surfaceGUI2.Face = face
-- local randomColor2 = BrickColor.random()
-- local frame1 = Instance.new("Frame")
-- frame1.Parent = surfaceGUI1
-- frame1.Size = UDim2.fromScale(1,1)
-- frame1.BackgroundColor = randomColor2
-- local frame2 = Instance.new("Frame")
-- frame2.Parent = surfaceGUI2
-- frame2.Size = UDim2.fromScale(1,1)
-- frame2.BackgroundColor = randomColor2
-- end
--end
end
end
end
Small Edit: I also was looking into possibly making custom SSAO, and modifying the effect to produce somewhat of an outline? but that probably would be too hard and not worth it since it may not look like an actual outline. Also feel free to suggest completely alternative methods to what I’m doing, I don’t mind rewriting the code or making something new entirely.