237 lines
7.0 KiB
Lua
237 lines
7.0 KiB
Lua
local FFlagUserCarCam do
|
|
local success, result = pcall(function()
|
|
return UserSettings():IsUserFeatureEnabled("UserCarCam")
|
|
end)
|
|
FFlagUserCarCam = success and result
|
|
end
|
|
|
|
local EPSILON = 1e-3
|
|
local PITCH_LIMIT = math.rad(80)
|
|
local YAW_DEFAULT = math.rad(0)
|
|
local ZOOM_MINIMUM = 0.5
|
|
local ZOOM_SENSITIVITY_CURVATURE = 0.5
|
|
|
|
local Players = game:GetService("Players")
|
|
local RunService = game:GetService("RunService")
|
|
|
|
local BaseCamera = require(script.Parent:WaitForChild("BaseCamera"))
|
|
local CameraInput = require(script.Parent:WaitForChild("CameraInput"))
|
|
local CameraUtils = require(script.Parent:WaitForChild("CameraUtils"))
|
|
local ZoomController = require(script.Parent:WaitForChild("ZoomController"))
|
|
local VehicleCameraCore = require(script:WaitForChild("VehicleCameraCore"))
|
|
local VehicleCameraConfig = require(script:WaitForChild("VehicleCameraConfig"))
|
|
|
|
local localPlayer = Players.LocalPlayer
|
|
|
|
local map = CameraUtils.map
|
|
local Spring = CameraUtils.Spring
|
|
local mapClamp = CameraUtils.mapClamp
|
|
local sanitizeAngle = CameraUtils.sanitizeAngle
|
|
|
|
-- pitch-axis rotational velocity of a part with a given CFrame and total RotVelocity
|
|
local function pitchVelocity(rotVel, cf)
|
|
return math.abs(cf.XVector:Dot(rotVel))
|
|
end
|
|
|
|
-- yaw-axis rotational velocity of a part with a given CFrame and total RotVelocity
|
|
local function yawVelocity(rotVel, cf)
|
|
return math.abs(cf.YVector:Dot(rotVel))
|
|
end
|
|
|
|
-- track physics solver time delta separately from the render loop to correctly synchronize time delta
|
|
local worldDt = 1/60
|
|
if FFlagUserCarCam then
|
|
RunService.Stepped:Connect(function(_, _worldDt)
|
|
worldDt = _worldDt
|
|
end)
|
|
end
|
|
|
|
local VehicleCamera = setmetatable({}, BaseCamera)
|
|
VehicleCamera.__index = VehicleCamera
|
|
|
|
function VehicleCamera.new()
|
|
assert(FFlagUserCarCam)
|
|
local self = setmetatable(BaseCamera.new(), VehicleCamera)
|
|
self:Reset()
|
|
return self
|
|
end
|
|
|
|
function VehicleCamera:Reset()
|
|
assert(FFlagUserCarCam)
|
|
self.vehicleCameraCore = VehicleCameraCore.new(self:GetSubjectCFrame())
|
|
self.pitchSpring = Spring.new(0, -math.rad(VehicleCameraConfig.pitchBaseAngle))
|
|
self.yawSpring = Spring.new(0, YAW_DEFAULT)
|
|
self.lastPanTick = 0
|
|
|
|
local camera = workspace.CurrentCamera
|
|
local cameraSubject = camera and camera.CameraSubject
|
|
|
|
assert(camera)
|
|
assert(cameraSubject)
|
|
assert(cameraSubject:IsA("VehicleSeat"))
|
|
|
|
local assemblyParts = cameraSubject:GetConnectedParts(true) -- passing true to recursively get all assembly parts
|
|
local assemblyPosition, assemblyRadius = CameraUtils.getLooseBoundingSphere(assemblyParts)
|
|
|
|
assemblyRadius = math.max(assemblyRadius, EPSILON)
|
|
|
|
self.assemblyRadius = assemblyRadius
|
|
self.assemblyOffset = cameraSubject.CFrame:Inverse()*assemblyPosition -- seat-space offset of the assembly bounding sphere center
|
|
|
|
self:_StepInitialZoom()
|
|
end
|
|
|
|
function VehicleCamera:_StepInitialZoom()
|
|
assert(FFlagUserCarCam)
|
|
self:SetCameraToSubjectDistance(math.max(
|
|
ZoomController.GetZoomRadius(),
|
|
self.assemblyRadius*VehicleCameraConfig.initialZoomRadiusMul
|
|
))
|
|
end
|
|
|
|
function VehicleCamera:_StepRotation(dt, vdotz)
|
|
local yawSpring = self.yawSpring
|
|
local pitchSpring = self.pitchSpring
|
|
|
|
local rotationInput = CameraInput.getRotation(true)
|
|
local dYaw = -rotationInput.X
|
|
local dPitch = -rotationInput.Y
|
|
|
|
yawSpring.pos = sanitizeAngle(yawSpring.pos + dYaw)
|
|
pitchSpring.pos = sanitizeAngle(math.clamp(pitchSpring.pos + dPitch, -PITCH_LIMIT, PITCH_LIMIT))
|
|
|
|
if CameraInput.getRotationActivated() then
|
|
self.lastPanTick = os.clock()
|
|
end
|
|
|
|
local pitchBaseAngle = -math.rad(VehicleCameraConfig.pitchBaseAngle)
|
|
local pitchDeadzoneAngle = math.rad(VehicleCameraConfig.pitchDeadzoneAngle)
|
|
|
|
if os.clock() - self.lastPanTick > VehicleCameraConfig.autocorrectDelay then
|
|
-- adjust autocorrect response based on forward velocity
|
|
local autocorrectResponse = mapClamp(
|
|
vdotz,
|
|
VehicleCameraConfig.autocorrectMinCarSpeed,
|
|
VehicleCameraConfig.autocorrectMaxCarSpeed,
|
|
0,
|
|
VehicleCameraConfig.autocorrectResponse
|
|
)
|
|
|
|
yawSpring.freq = autocorrectResponse
|
|
pitchSpring.freq = autocorrectResponse
|
|
|
|
-- zero out response under a threshold
|
|
if yawSpring.freq < EPSILON then
|
|
yawSpring.vel = 0
|
|
end
|
|
|
|
if pitchSpring.freq < EPSILON then
|
|
pitchSpring.vel = 0
|
|
end
|
|
|
|
if math.abs(sanitizeAngle(pitchBaseAngle - pitchSpring.pos)) <= pitchDeadzoneAngle then
|
|
-- do nothing within the deadzone
|
|
pitchSpring.goal = pitchSpring.pos
|
|
else
|
|
pitchSpring.goal = pitchBaseAngle
|
|
end
|
|
else
|
|
yawSpring.freq = 0
|
|
yawSpring.vel = 0
|
|
|
|
pitchSpring.freq = 0
|
|
pitchSpring.vel = 0
|
|
|
|
pitchSpring.goal = pitchBaseAngle
|
|
end
|
|
|
|
return CFrame.fromEulerAnglesYXZ(
|
|
pitchSpring:step(dt),
|
|
yawSpring:step(dt),
|
|
0
|
|
)
|
|
end
|
|
|
|
function VehicleCamera:_GetThirdPersonLocalOffset()
|
|
return self.assemblyOffset + Vector3.new(0, self.assemblyRadius*VehicleCameraConfig.verticalCenterOffset, 0)
|
|
end
|
|
|
|
function VehicleCamera:_GetFirstPersonLocalOffset(subjectCFrame)
|
|
local character = localPlayer.Character
|
|
|
|
if character and character.Parent then
|
|
local head = character:FindFirstChild("Head")
|
|
|
|
if head and head:IsA("BasePart") then
|
|
return subjectCFrame:Inverse()*head.Position
|
|
end
|
|
end
|
|
|
|
return self:_GetThirdPersonLocalOffset()
|
|
end
|
|
|
|
function VehicleCamera:Update()
|
|
assert(FFlagUserCarCam)
|
|
local camera = workspace.CurrentCamera
|
|
local cameraSubject = camera and camera.CameraSubject
|
|
local vehicleCameraCore = self.vehicleCameraCore
|
|
|
|
assert(camera)
|
|
assert(cameraSubject)
|
|
assert(cameraSubject:IsA("VehicleSeat"))
|
|
|
|
-- consume the physics solver time delta to account for mismatched physics/render cycles
|
|
local dt = worldDt
|
|
worldDt = 0
|
|
|
|
-- get subject info
|
|
local subjectCFrame = self:GetSubjectCFrame()
|
|
local subjectVel = self:GetSubjectVelocity()
|
|
local subjectRotVel = self:GetSubjectRotVelocity()
|
|
|
|
-- measure the local-to-world-space forward velocity of the vehicle
|
|
local vDotZ = math.abs(subjectVel:Dot(subjectCFrame.ZVector))
|
|
local yawVel = yawVelocity(subjectRotVel, subjectCFrame)
|
|
local pitchVel = pitchVelocity(subjectRotVel, subjectCFrame)
|
|
|
|
-- step camera components forward
|
|
local zoom = self:StepZoom()
|
|
local objectRotation = self:_StepRotation(dt, vDotZ)
|
|
|
|
-- mix third and first person offsets in local space
|
|
local firstPerson = mapClamp(zoom, ZOOM_MINIMUM, self.assemblyRadius, 1, 0)
|
|
|
|
local tpOffset = self:_GetThirdPersonLocalOffset()
|
|
local fpOffset = self:_GetFirstPersonLocalOffset(subjectCFrame)
|
|
local localOffset = tpOffset:Lerp(fpOffset, firstPerson)
|
|
|
|
-- step core forward
|
|
vehicleCameraCore:setTransform(subjectCFrame)
|
|
local processedRotation = vehicleCameraCore:step(dt, pitchVel, yawVel, firstPerson)
|
|
|
|
-- calculate final focus & cframe
|
|
local focus = CFrame.new(subjectCFrame*localOffset)*processedRotation*objectRotation
|
|
local cf = focus*CFrame.new(0, 0, zoom)
|
|
|
|
return cf, focus
|
|
end
|
|
|
|
function VehicleCamera:ApplyVRTransform()
|
|
assert(FFlagUserCarCam)
|
|
-- no-op override; VR transform is not applied in vehicles
|
|
end
|
|
|
|
function VehicleCamera:EnterFirstPerson()
|
|
assert(FFlagUserCarCam)
|
|
self.inFirstPerson = true
|
|
self:UpdateMouseBehavior()
|
|
end
|
|
|
|
function VehicleCamera:LeaveFirstPerson()
|
|
assert(FFlagUserCarCam)
|
|
self.inFirstPerson = false
|
|
self:UpdateMouseBehavior()
|
|
end
|
|
|
|
return VehicleCamera
|