This commit is contained in:
lx
2026-07-06 13:39:13 -04:00
commit 34bf440a8c
2694 changed files with 77580 additions and 0 deletions
+808
View File
@@ -0,0 +1,808 @@
--Panel3D: 3D GUI panels for VR
--written by 0xBAADF00D
--revised/refactored 5/11/16
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local GuiService = game:GetService("GuiService")
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local PlayersService = game.Players
--Util
local Util = {}
do
function Util.Create(instanceType)
return function(data)
local obj = Instance.new(instanceType)
for k, v in pairs(data) do
if type(k) == 'number' then
v.Parent = obj
else
obj[k] = v
end
end
return obj
end
end
-- RayPlaneIntersection (shortened)
-- http://www.siggraph.org/education/materials/HyperGraph/raytrace/rayplane_intersection.htm
function Util.RayPlaneIntersection(ray, planeNormal, pointOnPlane)
planeNormal = planeNormal.unit
ray = ray.Unit
local Vd = planeNormal:Dot(ray.Direction)
if Vd == 0 then -- parallel, no intersection
return nil
end
local V0 = planeNormal:Dot(pointOnPlane - ray.Origin)
local t = V0 / Vd
if t < 0 then --plane is behind ray origin, and thus there is no intersection
return nil
end
return ray.Origin + ray.Direction * t
end
end
--End of Util
--Panel3D State variables
local renderStepName = "Panel3DRenderStep-" .. game:GetService("HttpService"):GenerateGUID()
local defaultPixelsPerStud = 64
local pointUpCF = CFrame.Angles(math.rad(-90), math.rad(180), 0)
local zeroVector = Vector3.new(0, 0, 0)
local zeroVector2 = Vector2.new(0, 0)
local turnAroundCF = CFrame.Angles(0, math.rad(180), 0)
local fullyOpaqueAtPixelsFromEdge = 10
local fullyTransparentAtPixelsFromEdge = 80
local partThickness = 0.2
local cursorHidden = false
local cursorHideTime = 2.5
local currentModal = nil
local lastModal = nil
local currentMaxDist = math.huge
local currentClosest = nil
local lastClosest = nil
local currentHeadScale = 1
local panels = {}
local floorRotation = CFrame.new()
local cursor = Util.Create "ImageLabel" {
Image = "rbxasset://textures/Cursors/Gamepad/Pointer.png",
Size = UDim2.new(0, 8, 0, 8),
BackgroundTransparency = 1,
ZIndex = 10
}
--End of Panel3D State variables
--Panel3D Declaration and enumerations
local Panel3D = {}
Panel3D.Type = {
None = 0,
Floor = 1,
Fixed = 2,
HorizontalFollow = 3,
FixedToHead = 4
}
Panel3D.OnPanelClosed = Util.Create 'BindableEvent' {
Name = 'OnPanelClosed'
}
function Panel3D.GetHeadLookXZ(withTranslation)
local userHeadCF = UserInputService:GetUserCFrame(Enum.UserCFrame.Head)
local headLook = userHeadCF.lookVector
local headYaw = math.atan2(-headLook.Z, headLook.X) + math.rad(90)
local cf = CFrame.Angles(0, headYaw, 0)
if withTranslation then
cf = cf + userHeadCF.p
end
return cf
end
function Panel3D.FindContainerOf(element)
for i, v in pairs(panels) do
if v.gui and v.gui:IsAncestorOf(element) then
return v
end
end
return nil
end
function Panel3D.SetModalPanel(panel)
if currentModal == panel then
return
end
if currentModal then
currentModal:OnModalChanged(false)
end
if panel then
panel:OnModalChanged(true)
end
lastModal = currentModal
currentModal = panel
end
--End of Panel3D Declaration and enumerations
--Cursor autohiding methods
local cursorHidden = false
local hasTool = false
local lastMouseActivity = tick()
local lastMouseBehavior = Enum.MouseBehavior.Default
local function OnCharacterAdded(character)
hasTool = false
for i, v in ipairs(character:GetChildren()) do
if v:IsA("Tool") then
hasTool = true
end
end
character.ChildAdded:connect(function(child)
if child:IsA("Tool") then
hasTool = true
lastMouseActivity = tick() --kick the mouse when a tool is equipped
end
end)
character.ChildRemoved:connect(function(child)
if child:IsA("Tool") then
hasTool = false
end
end)
end
spawn(function()
while not PlayersService.LocalPlayer do wait() end
PlayersService.LocalPlayer.CharacterAdded:connect(OnCharacterAdded)
if PlayersService.LocalPlayer.Character then OnCharacterAdded(PlayersService.LocalPlayer.Character) end
end)
local function autoHideCursor(hide)
if not UserInputService.VREnabled then
cursorHidden = false
return
end
if hide then
--don't hide if there's a tool in the character
local character = PlayersService.LocalPlayer.Character
if character and hasTool then
return
end
cursorHidden = true
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide
else
cursorHidden = false
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.None
end
end
local function isCursorVisible()
--if ForceShow, the cursor is definitely visible at all times
if UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.ForceShow then
return true
end
--if ForceHide, the cursor is definitely NOT visible
if UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.ForceHide then
return false
end
--Otherwise, we need to check if the developer set MouseIconEnabled=false
if UserInputService.MouseIconEnabled and UserInputService.OverrideMouseIconBehavior == Enum.OverrideMouseIconBehavior.None then
return true
end
return false
end
--End of cursor autohiding methods
--Panel class implementation
local Panel = {}
Panel.mt = { __index = Panel }
function Panel.new(name)
local instance = {
name = name,
part = nil,
gui = nil,
width = 1,
height = 1,
isVisible = false,
isEnabled = false,
panelType = Panel3D.Type.None,
pixelScale = 1,
showCursor = true,
canFade = true,
shouldFindLookAtGuiElement = false,
ignoreModal = false,
linkedTo = nil,
subpanels = {},
transparency = 1,
forceShowUntilLookedAt = false,
isLookedAt = false,
isOffscreen = true,
lookAtPixel = Vector2.new(-1, -1),
lookAtDistance = math.huge,
lookAtGuiElement = nil,
isClosest = true
}
if panels[name] then
error("A panel by the name of " .. name .. " already exists.")
end
panels[name] = instance
return setmetatable(instance, Panel.mt)
end
--Panel accessor methods
function Panel:GetPart()
if not self.part then
self.part = Util.Create "Part" {
Name = self.name,
Parent = nil,
Transparency = 1,
CanCollide = false,
Anchored = true,
Archivable = false,
Size = Vector3.new(1, 1, partThickness)
}
end
return self.part
end
function Panel:GetGUI()
if not self.gui then
local part = self:GetPart()
self.gui = Util.Create "SurfaceGui" {
Parent = CoreGui,
Adornee = part,
Active = true,
ToolPunchThroughDistance = 1000,
CanvasSize = Vector2.new(0, 0),
Enabled = self.isEnabled,
AlwaysOnTop = true
}
end
return self.gui
end
function Panel:FindHoveredGuiElement(elements)
local x, y = self.lookAtPixel.X, self.lookAtPixel.Y
for i, v in pairs(elements) do
local minPt = v.AbsolutePosition
local maxPt = v.AbsolutePosition + v.AbsoluteSize
if minPt.X <= x and maxPt.X >= x and
minPt.Y <= y and maxPt.Y >= y then
return v, i
end
end
end
--End of panel accessor methods
--Panel update methods
function Panel:SetPartCFrame(cframe)
self:GetPart().CFrame = cframe * CFrame.new(0, 0, -0.5 * partThickness)
end
function Panel:SetEnabled(enabled)
if self.isEnabled == enabled then
return
end
self.isEnabled = enabled
if enabled then
self:GetPart().Parent = workspace.CurrentCamera --todo: Perhaps this can change soon.
self:GetGUI().Enabled = true
for i, v in pairs(self.subpanels) do
v:GetPart().Parent = workspace.CurrentCamera
v:GetGUI().Enabled = true
end
else
self:GetPart().Parent = nil
self:GetGUI().Enabled = false
for i, v in pairs(self.subpanels) do
v:GetPart().Parent = nil
v:GetGUI().Enabled = false
end
end
self:OnEnabled(enabled)
end
function Panel:EvaluatePositioning(cameraCF, cameraRenderCF, userHeadCF)
if self.panelType == Panel3D.Type.Floor then
--Floor panels simply... go on the floor.
--Panel will be in camera's local space (which is assumed to be horizontal),
--and floorRotation is derived from the user's head rotation (yaw only)
local floorCF = cameraCF * CFrame.new(0, -headHeightFromFloor * currentHeadScale, 0) * floorRotation
self:SetPartCFrame(floorCF * pointUpCF)
elseif self.panelType == Panel3D.Type.Fixed then
--Places the panel in the camera's local space, but doesn't follow the user's head.
--Useful if you know what you're doing. localCF can be updated in PreUpdate for animation.
local cf = self.localCF - self.localCF.p
cf = cf + (self.localCF.p * currentHeadScale)
self:SetPartCFrame(cameraCF * cf)
elseif self.panelType == Panel3D.Type.HorizontalFollow then
local headLook = userHeadCF.lookVector
local headYaw = math.atan2(-headLook.Z, headLook.X) + math.rad(90)
local headForwardCF = CFrame.Angles(0, headYaw, 0) + userHeadCF.p
local localCF = (headForwardCF * self.angleFromForward) * --Rotate about Y (left-right)
self.angleFromHorizon * --Rotate about X (up-down)
(currentHeadScale * self.distance) * --Move into scene
turnAroundCF --Turn around to face character
self:SetPartCFrame(cameraCF * localCF)
elseif self.panelType == Panel3D.Type.FixedToHead then
--Places the panel in the user's head local space. localCF can be updated in PreUpdate for animation.
local cf = self.localCF - self.localCF.p
cf = cf + (self.localCF.p * currentHeadScale)
self:SetPartCFrame(cameraRenderCF * cf)
end
end
function Panel:EvaluateGaze(cameraCF, cameraRenderCF, userHeadCF, lookRay)
--reset distance data
self.isClosest = false
self.lookAtPixel = zeroVector2
self.lookAtDistance = math.huge
--Evaluate the lookRay versus this panel
local planeCF = self:GetPart().CFrame
local planeNormal = planeCF.lookVector
local pointOnPlane = planeCF.p + (planeNormal * partThickness * 0.5) --Move the point out by half the thickness of the part
local worldIntersectPoint = Util.RayPlaneIntersection(lookRay, planeNormal, pointOnPlane)
if worldIntersectPoint then
self.isOffscreen = false
--transform worldIntersectPoint to gui space
local gui = self:GetGUI()
local guiWidth, guiHeight = gui.AbsoluteSize.X, gui.AbsoluteSize.Y
local localIntersectPoint = (planeCF:pointToObjectSpace(worldIntersectPoint) / currentHeadScale) * Vector3.new(-1, 1, 1) + Vector3.new(self.width / 2, -self.height / 2, 0)
self.lookAtPixel = Vector2.new((localIntersectPoint.X / self.width) * gui.AbsoluteSize.X, (localIntersectPoint.Y / self.height) * -gui.AbsoluteSize.Y)
--fire mouse enter/leave events if necessary
local guiX, guiY = self.lookAtPixel.X, self.lookAtPixel.Y
if guiX >= 0 and guiX <= guiWidth and
guiY >= 0 and guiY <= guiHeight then
if not self.isLookedAt then
self.isLookedAt = true
self:OnMouseEnter(guiX, guiY)
if self.forceShowUntilLookedAt then
self.forceShowUntilLookedAt = false
end
end
else
if self.isLookedAt then
self.isLookedAt = false
self:OnMouseLeave(guiX, guiY)
end
end
--evaluate distance
self.lookAtDistance = (worldIntersectPoint - cameraRenderCF.p).magnitude
if self.isLookedAt and self.lookAtDistance < currentMaxDist and self.showCursor then
currentMaxDist = self.lookAtDistance
currentClosest = self
end
else
self.isOffscreen = true
--Not looking at the plane at all, so fire off mouseleave if necessary.
if self.isLookedAt then
self.isLookedAt = false
self:OnMouseLeave(self.lookAtPixel.X, self.lookAtPixel.Y)
end
end
end
function Panel:EvaluateTransparency()
--Early exit if force shown
if self.forceShowUntilLookedAt then
self.transparency = 0
return
end
--Early exit if we're looking at the panel (no transparency!)
if self.isLookedAt then
self.transparency = 0
return
end
--Similarly, exit if we can't possibly see the panel.
if self.isOffscreen then
self.transparency = 1
return
end
--Otherwise, we'll want to calculate the transparency.
self.transparency = self:CalculateTransparency()
end
function Panel:Update(cameraCF, cameraRenderCF, userHeadCF, lookRay)
if self.forceShowUntilLookedAt and not self.part then
self:GetPart()
self:GetGUI()
end
if not self.part then
return
end
local isModal = (currentModal == self)
if not isModal and self.linkedTo and self.linkedTo == currentModal then
isModal = true
end
if currentModal and not isModal then
self:SetEnabled(false)
return
end
self:PreUpdate(cameraCF, cameraRenderCF, userHeadCF, lookRay)
if self.isVisible then
self:EvaluatePositioning(cameraCF, cameraRenderCF, userHeadCF)
self:EvaluateGaze(cameraCF, cameraRenderCF, userHeadCF, lookRay)
self:EvaluateTransparency(cameraCF, cameraRenderCF)
end
end
--End of Panel update methods
--Panel virtual methods
function Panel:PreUpdate() --virtual: handle positioning here
end
function Panel:OnUpdate() --virtual: handle transparency here
end
function Panel:OnMouseEnter(x, y) --virtual
end
function Panel:OnMouseLeave(x, y) --virtual
end
function Panel:OnEnabled(enabled) --virtual
end
function Panel:OnModalChanged(isModal) --virtual
end
function Panel:OnVisibilityChanged(visible) --virtual
end
function Panel:CalculateTransparency() --virtual
if not self.canFade then
return 0
end
local guiWidth, guiHeight = self.gui.AbsoluteSize.X, self.gui.AbsoluteSize.Y
local lookX, lookY = self.lookAtPixel.X, self.lookAtPixel.Y
--Determine the distance from the edge;
--if x is negative it's on the left side, meaning the distance is just absolute value
--if x is positive it's on the right side, meaning the distance is x minus the width
local xEdgeDist = lookX < 0 and -lookX or (lookX - guiWidth)
local yEdgeDist = lookY < 0 and -lookY or (lookY - guiHeight)
if lookX > 0 and lookX < guiWidth then
xEdgeDist = 0
end
if lookY > 0 and lookY < guiHeight then
yEdgeDist = 0
end
local edgeDist = math.sqrt(xEdgeDist ^ 2 + yEdgeDist ^ 2)
--since transparency is 0-1, we know how many pixels will give us 0 and how many will give us 1.
local offset = fullyOpaqueAtPixelsFromEdge
local interval = fullyTransparentAtPixelsFromEdge
--then we just clamp between 0 and 1.
return math.max(0, math.min(1, (edgeDist - offset) / interval))
end
--End of Panel virtual methods
--Panel configuration methods
function Panel:ResizeStuds(width, height, pixelsPerStud)
pixelsPerStud = pixelsPerStud or defaultPixelsPerStud
self.width = width
self.height = height
self.pixelScale = pixelsPerStud / defaultPixelsPerStud
local part = self:GetPart()
part.Size = Vector3.new(self.width * currentHeadScale, self.height * currentHeadScale, partThickness)
local gui = self:GetGUI()
gui.CanvasSize = Vector2.new(pixelsPerStud * self.width, pixelsPerStud * self.height)
end
function Panel:ResizePixels(width, height, pixelsPerStud)
pixelsPerStud = pixelsPerStud or defaultPixelsPerStud
local widthInStuds = width / pixelsPerStud
local heightInStuds = height / pixelsPerStud
self:ResizeStuds(widthInStuds, heightInStuds, pixelsPerStud)
end
function Panel:OnHeadScaleChanged(newHeadScale)
local pixelsPerStud = self.pixelScale * defaultPixelsPerStud
self:ResizeStuds(self.width, self.height, pixelsPerStud)
end
function Panel:SetType(panelType, config)
self.panelType = panelType
--clear out old type-specific members
self.floorPos = nil
self.localCF = nil
self.angleFromHorizon = nil
self.angleFromForward = nil
self.distance = nil
if not config then
config = {}
end
if panelType == Panel3D.Type.None then
--nothing to do
return
elseif panelType == Panel3D.Type.Floor then
self.floorPos = config.FloorPosition or Vector3.new(0, 0, 0)
elseif panelType == Panel3D.Type.Fixed then
self.localCF = config.CFrame or CFrame.new()
elseif panelType == Panel3D.Type.HorizontalFollow then
self.angleFromHorizon = CFrame.Angles(config.angleFromHorizon or 0, 0, 0)
self.angleFromForward = CFrame.Angles(0, config.angleFromForward or 0, 0)
self.distance = CFrame.new(0, 0, config.distance or 5)
elseif panelType == Panel3D.Type.FixedToHead then
self.localCF = config.CFrame or CFrame.new()
else
error("Invalid Panel type")
end
end
function Panel:SetVisible(visible, modal)
if visible ~= self.isVisible then
self:OnVisibilityChanged(visible)
if not visible then
Panel3D.OnPanelClosed:Fire(self.name)
end
end
self.isVisible = visible
self:SetEnabled(visible)
if visible and modal then
Panel3D.SetModalPanel(self)
end
if not visible and currentModal == self then
if modal and not doNotRestore then
--restore last modal panel
Panel3D.SetModalPanel(lastModal)
else
Panel3D.SetModalPanel(nil)
--if the coder explicitly wanted to hide this modal panel,
--it follows that they don't want it to be restored when the next
--modal panel is hidden.
if lastModal == self then
lastModal = nil
end
end
end
if not visible and self.forceShowUntilLookedAt then
self.forceShowUntilLookedAt = false
end
end
function Panel:LinkTo(panelName)
if type(panelName) == "string" then
self.linkedTo = Panel3D.Get(panelName)
else
self.linkedTo = panelName
end
end
function Panel:ForceShowUntilLookedAt(makeModal)
--ensure the part exists
self:GetPart()
self:GetGUI()
self:SetVisible(true, makeModal)
self.forceShowUntilLookedAt = true
end
function Panel:SetCanFade(canFade)
self.canFade = canFade
end
--Child class, Subpanel
local Subpanel = {}
local Subpanel_mt = {}
function Subpanel.new(guiElement)
local instance = {
guiElement = guiElement
}
return setmetatable(instance, Subpanel_mt)
end
function Subpanel:GetPart()
warn("Subpanel:GetPart() not yet implemented")
return nil
end
function Subpanel:GetGUI()
warn("Subpanel:GetGUI() not yet implemented")
return nil
end
function Panel:AddSubpanel(guiElement)
local subpanel = Subpanel.new(guiElement)
self.subpanels[guiElement] = subpanel
end
function Panel:RemoveSubpanel(guiElement)
self.subpanels[guiElement] = nil
end
--End of Panel configuration methods
--End of Panel class implementation
--Panel3D API
function Panel3D.Get(name)
local panel = panels[name]
if not panels[name] then
panels[name] = Panel.new(name)
panel = panels[name]
end
return panel
end
--End of Panel3D API
--Panel3D Setup
local function onRenderStep()
if not UserInputService.VREnabled then
return
end
--reset distance info
currentClosest = nil
currentMaxDist = math.huge
--figure out some useful stuff
local camera = workspace.CurrentCamera
local cameraCF = camera.CFrame
local cameraRenderCF = camera:GetRenderCFrame()
local userHeadCF = UserInputService:GetUserCFrame(Enum.UserCFrame.Head)
local lookRay = Ray.new(cameraRenderCF.p, cameraRenderCF.lookVector)
--allow all panels to run their own update code
for i, v in pairs(panels) do
v:Update(cameraCF, cameraRenderCF, userHeadCF, lookRay)
end
--evaluate linked panels
local processed = {}
for i, v in pairs(panels) do
if not processed[v] and v.linkedTo and v.isVisible and v.linkedTo.isVisible then
processed[v] = true
processed[v.linkedTo] = true
local minTransparency = math.min(v.transparency, v.linkedTo.transparency)
v.transparency = minTransparency
v.linkedTo.transparency = minTransparency
end
end
--run post update because the distance information hasn't been
--finalized until now.
for i, v in pairs(panels) do
--If the part is fully transparent, we don't want to keep it around in the workspace.
if v.part and v.gui then
--check if this panel is the current modal panel
local isModal = (currentModal == v)
--but also check if this panel is linked to the current modal panel
if not isModal and v.linkedTo and v.linkedTo == currentModal then
isModal = true
end
local show = v.isVisible
if not isModal and currentModal then
show = false
end
if v.transparency >= 1 then
show = false
end
if v.forceShowUntilLookedAt then
show = true
end
if not v.canFade and v.isVisible then
show = true
end
v:SetEnabled(show)
end
v:OnUpdate()
end
--place the cursor on the closest panel (for now)
if not currentClosest and lastClosest then
UserInputService.MouseBehavior = lastMouseBehavior
elseif currentClosest and not lastClosest then
lastMouseBehavior = UserInputService.MouseBehavior
end
if currentClosest then
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
UserInputService.OverrideMouseIconBehavior = Enum.OverrideMouseIconBehavior.ForceHide
cursor.Parent = currentClosest:GetGUI()
local x, y = currentClosest.lookAtPixel.X, currentClosest.lookAtPixel.Y
cursor.Size = UDim2.new(0, 8 * currentClosest.pixelScale, 0, 8 * currentClosest.pixelScale)
cursor.Position = UDim2.new(0, x - cursor.AbsoluteSize.x * 0.5, 0, y - cursor.AbsoluteSize.y * 0.5)
else
cursor.Parent = nil
end
lastClosest = currentClosest
end
RunService:BindToRenderStep(renderStepName, Enum.RenderPriority.Last.Value, onRenderStep)
--Implement cursor autohide functionality
UserInputService.InputChanged:connect(function(inputObj, processed)
if inputObj.UserInputType == Enum.UserInputType.MouseMovement then
lastMouseActivity = tick()
autoHideCursor(false)
end
end)
local function onHeartbeat()
if isCursorVisible() then
cursorHidden = false
end
if lastMouseActivity + cursorHideTime < tick() and not GuiService.MenuIsOpen and not cursorHidden then
autoHideCursor(true)
end
end
RunService.Heartbeat:connect(onHeartbeat)
--Handle camera changes
local cameraChangedConnection = nil
local function onCameraChanged(prop)
if prop == "HeadScale" then
pcall(function()
currentHeadScale = workspace.CurrentCamera.HeadScale
end)
for i, v in pairs(panels) do
v:OnHeadScaleChanged(currentHeadScale)
end
end
end
local function onWorkspaceChanged(prop)
if prop == "CurrentCamera" then
onCameraChanged("HeadScale")
if cameraChangedConnection then
cameraChangedConnection:disconnect()
end
cameraChangedConnection = workspace.CurrentCamera.Changed:connect(onCameraChanged)
end
end
if workspace.CurrentCamera then
onWorkspaceChanged("CurrentCamera")
end
workspace.Changed:connect(onWorkspaceChanged)
return Panel3D