EDIT: CurrentDistance doesn’t update when the billboard is disabled, therefore, the code below is still valid when you need to figure out distance on disabled billboards for custom distance culling systems for pop-in/pop-out effects.
This is still appears to be broken as CurrentDistance sometimes doesn’t even update its value, making this property extremely unreliable to work with.
In the meantime, you can manually calculate a BillboardGui’s position in the world through getBillboardWorldPos below, which takes into account all offset properties, and also returns the distance from the camera:
local HUGE = math.huge
local function getBoundingBox(instance)
if instance:IsA("BasePart") then
return instance.CFrame, instance.Size
elseif instance:IsA("Model") then
return instance:GetBoundingBox()
end
error(`invalid bounding box instance '{instance:GetFullName()}'`, 2)
end
local function getCameraAlignedBoundingBox(instance, camera)
debug.profilebegin("getCameraAlignedBoundingBox")
local camera = camera or workspace.CurrentCamera
if not camera then
error(`missing camera`, 2)
end
local baseCFrame, size = getBoundingBox(instance)
local halfSize = size * 0.5
local corners = {
Vector3.new(-halfSize.X, -halfSize.Y, -halfSize.Z),
Vector3.new(-halfSize.X, -halfSize.Y, halfSize.Z),
Vector3.new(-halfSize.X, halfSize.Y, -halfSize.Z),
Vector3.new(-halfSize.X, halfSize.Y, halfSize.Z),
Vector3.new( halfSize.X, -halfSize.Y, -halfSize.Z),
Vector3.new( halfSize.X, -halfSize.Y, halfSize.Z),
Vector3.new( halfSize.X, halfSize.Y, -halfSize.Z),
Vector3.new( halfSize.X, halfSize.Y, halfSize.Z),
}
local camCF = camera.CFrame
local minX, minY, minZ = HUGE, HUGE, HUGE
local maxX, maxY, maxZ = -HUGE, -HUGE, -HUGE
for _, cornerLocal in ipairs(corners) do
local cornerWorld = baseCFrame:PointToWorldSpace(cornerLocal)
local cornerCam = camCF:PointToObjectSpace(cornerWorld)
minX = if cornerCam.X < minX then cornerCam.X else minX
minY = if cornerCam.Y < minY then cornerCam.Y else minY
minZ = if cornerCam.Z < minZ then cornerCam.Z else minZ
maxX = if cornerCam.X > maxX then cornerCam.X else maxX
maxY = if cornerCam.Y > maxY then cornerCam.Y else maxY
maxZ = if cornerCam.Z > maxZ then cornerCam.Z else maxZ
end
local minCam = Vector3.new(minX, minY, minZ)
local maxCam = Vector3.new(maxX, maxY, maxZ)
local centerCam = (minCam + maxCam) * 0.5
local extentCam = maxCam - minCam
local centerWorld = camCF:PointToWorldSpace(centerCam)
local rotationOnly = camCF - camCF.Position
local boundingBoxCF = rotationOnly + centerWorld
debug.profileend()
return boundingBoxCF, extentCam
end
local function getBillboardWorldPos(billboard, camera)
local adornee = (billboard.Adornee or billboard.Parent)
if not adornee then
error(`billboard '{billboard:GetFullName()}' missing adornee`, 2)
end
debug.profilebegin("getBillboardWorldPosition")
local adorneeCFrame
if adornee:IsA("Attachment") then
adorneeCFrame = adornee.WorldCFrame
elseif adornee:IsA("BasePart") then
adorneeCFrame = adornee.CFrame
elseif adornee:IsA("Model") then
-- For a model, Roblox uses bounding-box CFrame instead of :GetPivot()
local cf, _ = adornee:GetBoundingBox()
adorneeCFrame = cf
else
debug.profileend()
error(`billboard '{billboard:GetFullName()}' missing valid adornee`, 2)
end
local camera = camera or workspace.CurrentCamera
if not camera then
debug.profileend()
error(`missing camera`, 2)
end
local finalPos = adorneeCFrame.Position
-- StudsOffsetWorldSpace is a bit misleading because the the offset is applied based on the adornee's axes and not the world axes.
-- But technically the Vector offset is in world space
finalPos = finalPos + adorneeCFrame:VectorToWorldSpace(billboard.StudsOffsetWorldSpace)
-- Offset based on camera's orientation (excluding position since the camera is not the adornee)
local camCFrame = camera.CFrame
local camPos = camCFrame.Position
local camRot = camCFrame - camPos
local studsOffset = billboard.StudsOffset
local studsOffsetWorld = camRot:VectorToWorldSpace(studsOffset)
finalPos = finalPos + studsOffsetWorld
-- Extents properties are ignored for attachments!
if not adornee:IsA("Attachment") then
-- EXTENTS OFFSET WORLD SPACE --
if billboard.ExtentsOffsetWorldSpace ~= Vector3.zero then
local offset = billboard.ExtentsOffsetWorldSpace
-- Get the bounding-box size in world axes
local cframe, sizeWS = getBoundingBox(adornee)
local halfSizeWS = sizeWS * 0.5
-- Multiply per-axis
local offset = Vector3.new(
halfSizeWS.X * offset.X,
halfSizeWS.Y * offset.Y,
halfSizeWS.Z * offset.Z
)
offset = cframe:VectorToWorldSpace(offset)
finalPos += offset
end
-- EXTENTS OFFSET --
if billboard.ExtentsOffset ~= Vector3.zero then
local offset = billboard.ExtentsOffset
-- Get camera-aligned bounding box size
local _, sizeCam = getCameraAlignedBoundingBox(adornee, camera)
local halfSizeCam = sizeCam * 0.5
-- Scale the offset by half the camera bounding box
local offsetCamSpace = Vector3.new(
halfSizeCam.X * offset.X,
halfSizeCam.Y * offset.Y,
halfSizeCam.Z * offset.Z
)
-- Rotate from camera space to world space
-- We'll get pure rotation from the camera by subtracting translation:
local camCFrame = camera.CFrame
local camRotOnly = camCFrame - camCFrame.Position
offset = camRotOnly:VectorToWorldSpace(offsetCamSpace)
finalPos += offset
end
end
local distance = (finalPos - camPos).Magnitude
debug.profileend()
return finalPos, distance
end