BillboardGui.CurrentDistance doesn't work

Referencing this post: Release Notes for 382

After setting BillboardGui.DistanceStep to 1, the property BillboardGui.CurrentDistance doesn’t appear to work as expected.

Observed Behavior

  • CurrentDistance always returns 0, regardless of the actual distance between the BillboardGui and the player’s camera.
  • The Changed event tied to this property does not fire either.

Expected Behavior

  • CurrentDistance should return the actual distance between the camera and the BillboardGui.
  • It should also fire the Changed event when the distance changes, especially when DistanceStep is set to 1 to allow for fine-grained updates.
2 Likes

Hey Kitsune,

Thanks for the detailed report!

As part of this fix, we will be officially deprecating DistanceStep, DistanceLowerLimit, and DistanceUpperLimit, as their intended functionality is better handled by developers in a LocalScript.

We are actively working on a fix for CurrentDistance. The intended behavior is for this property to report the distance between the player’s camera and the BillboardGui and to fire the Changed event whenever that distance updates. We’ll be updating the developer documentation to reflect this as well. To ensure the fix meets your needs, could you share your use case? Specifically, are you looking for:

  • Absolute Distance: The direct, straight-line distance between the camera and the GUI in 3D space.
  • Projected Distance: The distance from the camera to the GUI along the camera’s forward direction (essentially its Z-depth).

Your feedback will help us decide on the best implementation. Thanks!

So far, it still hasn’t been fixed. I guess you think that few people use it, so it has been forgotten. If the content in the document is displayed, you should implement the described function in the studio instead of asking us developers to waste time writing a bunch of unusable functions.

3 Likes

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
2 Likes