add gs
This commit is contained in:
+1587
File diff suppressed because it is too large
Load Diff
+1099
File diff suppressed because it is too large
Load Diff
+584
@@ -0,0 +1,584 @@
|
||||
--[[
|
||||
Invisicam
|
||||
|
||||
Modified 5.16.2017 by AllYourBlox to consider combined transparency when looking through multiple parts, and to add
|
||||
a mode that takes advantage of the reduced cost of ray casts from re-implementing GetPartsObscuringTarget
|
||||
on the C++ side to be O(N) for N parts hit, rather than O(N^2), and to have optimizations specifically
|
||||
improving performance of closely bundled rays. Fading is also removed, since it is a frame rate killer with high-poly models,
|
||||
and on mobile.
|
||||
|
||||
Based on Invisicam Version 2.5 by OnlyTwentyCharacters
|
||||
--]]
|
||||
|
||||
local Invisicam = {}
|
||||
---------------
|
||||
-- Constants --
|
||||
---------------
|
||||
local USE_STACKING_TRANSPARENCY = true -- Multiple items between the subject and camera get transparency values that add up to TARGET_TRANSPARENCY
|
||||
local TARGET_TRANSPARENCY = 0.75 -- Classic Invisicam's Value, also used by new invisicam for parts hit by head and torso rays
|
||||
local TARGET_TRANSPARENCY_PERIPHERAL = 0.5 -- Used by new SMART_CIRCLE mode for items not hit by head and torso rays
|
||||
|
||||
local MODE = {
|
||||
CUSTOM = 1, -- Whatever you want!
|
||||
LIMBS = 2, -- Track limbs
|
||||
MOVEMENT = 3, -- Track movement
|
||||
CORNERS = 4, -- Char model corners
|
||||
CIRCLE1 = 5, -- Circle of casts around character
|
||||
CIRCLE2 = 6, -- Circle of casts around character, camera relative
|
||||
LIMBMOVE = 7, -- LIMBS mode + MOVEMENT mode
|
||||
SMART_CIRCLE = 8, -- More sample points on and around character
|
||||
CHAR_OUTLINE = 9,
|
||||
}
|
||||
|
||||
Invisicam.MODE = MODE
|
||||
local STARTING_MODE = MODE.SMART_CIRCLE
|
||||
|
||||
local LIMB_TRACKING_SET = {
|
||||
-- Common to R6, R15
|
||||
['Head'] = true,
|
||||
|
||||
-- R6 Only
|
||||
['Left Arm'] = true,
|
||||
['Right Arm'] = true,
|
||||
['Left Leg'] = true,
|
||||
['Right Leg'] = true,
|
||||
|
||||
-- R15 Only
|
||||
['LeftLowerArm'] = true,
|
||||
['RightLowerArm'] = true,
|
||||
['LeftUpperLeg'] = true,
|
||||
['RightUpperLeg'] = true
|
||||
}
|
||||
|
||||
local CORNER_FACTORS = {
|
||||
Vector3.new(1,1,-1),
|
||||
Vector3.new(1,-1,-1),
|
||||
Vector3.new(-1,-1,-1),
|
||||
Vector3.new(-1,1,-1)
|
||||
}
|
||||
|
||||
local CIRCLE_CASTS = 10
|
||||
local MOVE_CASTS = 3
|
||||
local SMART_CIRCLE_CASTS = 24
|
||||
local SMART_CIRCLE_INCREMENT = 2.0 * math.pi / SMART_CIRCLE_CASTS
|
||||
local CHAR_OUTLINE_CASTS = 24
|
||||
|
||||
---------------
|
||||
-- Variables --
|
||||
---------------
|
||||
|
||||
local RunService = game:GetService('RunService')
|
||||
local PlayersService = game:GetService('Players')
|
||||
local Player = PlayersService.LocalPlayer
|
||||
|
||||
local Camera = nil
|
||||
local Character = nil
|
||||
local HumanoidRootPart = nil
|
||||
local TorsoPart = nil
|
||||
local HeadPart = nil
|
||||
|
||||
local Mode = nil
|
||||
local BehaviorFunction = nil
|
||||
|
||||
local childAddedConn = nil
|
||||
local childRemovedConn = nil
|
||||
|
||||
local Behaviors = {} -- Map of modes to behavior fns
|
||||
local SavedHits = {} -- Objects currently being faded in/out
|
||||
local TrackedLimbs = {} -- Used in limb-tracking casting modes
|
||||
|
||||
---------------
|
||||
--| Utility |--
|
||||
---------------
|
||||
|
||||
local math_min = math.min
|
||||
local math_max = math.max
|
||||
local math_cos = math.cos
|
||||
local math_sin = math.sin
|
||||
local math_pi = math.pi
|
||||
|
||||
local Vector3_new = Vector3.new
|
||||
local ZERO_VECTOR3 = Vector3_new(0,0,0)
|
||||
|
||||
local function AssertTypes(param, ...)
|
||||
local allowedTypes = {}
|
||||
local typeString = ''
|
||||
for _, typeName in pairs({...}) do
|
||||
allowedTypes[typeName] = true
|
||||
typeString = typeString .. (typeString == '' and '' or ' or ') .. typeName
|
||||
end
|
||||
local theType = type(param)
|
||||
assert(allowedTypes[theType], typeString .. " type expected, got: " .. theType)
|
||||
end
|
||||
|
||||
-----------------------
|
||||
--| Local Functions |--
|
||||
-----------------------
|
||||
|
||||
local function LimbBehavior(castPoints)
|
||||
for limb, _ in pairs(TrackedLimbs) do
|
||||
castPoints[#castPoints + 1] = limb.Position
|
||||
end
|
||||
end
|
||||
|
||||
local function MoveBehavior(castPoints)
|
||||
for i = 1, MOVE_CASTS do
|
||||
local position, velocity = HumanoidRootPart.Position, HumanoidRootPart.Velocity
|
||||
local horizontalSpeed = Vector3_new(velocity.X, 0, velocity.Z).Magnitude / 2
|
||||
local offsetVector = (i - 1) * HumanoidRootPart.CFrame.lookVector * horizontalSpeed
|
||||
castPoints[#castPoints + 1] = position + offsetVector
|
||||
end
|
||||
end
|
||||
|
||||
local function CornerBehavior(castPoints)
|
||||
local cframe = HumanoidRootPart.CFrame
|
||||
local centerPoint = cframe.p
|
||||
local rotation = cframe - centerPoint
|
||||
local halfSize = Character:GetExtentsSize() / 2 --NOTE: Doesn't update w/ limb animations
|
||||
castPoints[#castPoints + 1] = centerPoint
|
||||
for i = 1, #CORNER_FACTORS do
|
||||
castPoints[#castPoints + 1] = centerPoint + (rotation * (halfSize * CORNER_FACTORS[i]))
|
||||
end
|
||||
end
|
||||
|
||||
local function CircleBehavior(castPoints)
|
||||
local cframe = nil
|
||||
if Mode == MODE.CIRCLE1 then
|
||||
cframe = HumanoidRootPart.CFrame
|
||||
else
|
||||
local camCFrame = Camera.CoordinateFrame
|
||||
cframe = camCFrame - camCFrame.p + HumanoidRootPart.Position
|
||||
end
|
||||
castPoints[#castPoints + 1] = cframe.p
|
||||
for i = 0, CIRCLE_CASTS - 1 do
|
||||
local angle = (2 * math_pi / CIRCLE_CASTS) * i
|
||||
local offset = 3 * Vector3_new(math_cos(angle), math_sin(angle), 0)
|
||||
castPoints[#castPoints + 1] = cframe * offset
|
||||
end
|
||||
end
|
||||
|
||||
local function LimbMoveBehavior(castPoints)
|
||||
LimbBehavior(castPoints)
|
||||
MoveBehavior(castPoints)
|
||||
end
|
||||
|
||||
local function CharacterOutlineBehavior(castPoints)
|
||||
local torsoUp = TorsoPart.CFrame.upVector.unit
|
||||
local torsoRight = TorsoPart.CFrame.rightVector.unit
|
||||
|
||||
-- Torso cross of points for interior coverage
|
||||
castPoints[#castPoints + 1] = TorsoPart.CFrame.p
|
||||
castPoints[#castPoints + 1] = TorsoPart.CFrame.p + torsoUp
|
||||
castPoints[#castPoints + 1] = TorsoPart.CFrame.p - torsoUp
|
||||
castPoints[#castPoints + 1] = TorsoPart.CFrame.p + torsoRight
|
||||
castPoints[#castPoints + 1] = TorsoPart.CFrame.p - torsoRight
|
||||
if HeadPart then
|
||||
castPoints[#castPoints + 1] = HeadPart.CFrame.p
|
||||
end
|
||||
|
||||
local cframe = CFrame.new(ZERO_VECTOR3,Vector3_new(Camera.CoordinateFrame.lookVector.X,0,Camera.CoordinateFrame.lookVector.Z))
|
||||
local centerPoint = (TorsoPart and TorsoPart.Position or HumanoidRootPart.Position)
|
||||
|
||||
local partsWhitelist = {TorsoPart}
|
||||
if HeadPart then
|
||||
partsWhitelist[#partsWhitelist + 1] = HeadPart
|
||||
end
|
||||
|
||||
for i = 1, CHAR_OUTLINE_CASTS do
|
||||
local angle = (2 * math_pi * i / CHAR_OUTLINE_CASTS)
|
||||
local offset = cframe * (3 * Vector3_new(math_cos(angle), math_sin(angle), 0))
|
||||
|
||||
offset = Vector3_new(offset.X, math_max(offset.Y, -2.25), offset.Z)
|
||||
|
||||
local ray = Ray.new(centerPoint + offset, -3 * offset)
|
||||
local hit, hitPoint = game.Workspace:FindPartOnRayWithWhitelist(ray, partsWhitelist, false, false)
|
||||
|
||||
if hit then
|
||||
-- Use hit point as the cast point, but nudge it slightly inside the character so that bumping up against
|
||||
-- walls is less likely to cause a transparency glitch
|
||||
castPoints[#castPoints + 1] = hitPoint + 0.2 * (centerPoint - hitPoint).unit
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Helper function for Determinant of 3x3
|
||||
local function Det3x3(a,b,c,d,e,f,g,h,i)
|
||||
return (a*(e*i-f*h)-b*(d*i-f*g)+c*(d*h-e*g))
|
||||
end
|
||||
|
||||
-- Smart Circle mode needs the intersection of 2 rays that are known to be in the same plane
|
||||
-- because they are generated from cross products with a common vector. This function is computing
|
||||
-- that intersection, but it's actually the general solution for the point halfway between where
|
||||
-- two skew lines come nearest to each other, which is more forgiving.
|
||||
local function RayIntersection(p0, v0, p1, v1)
|
||||
local v2 = v0:Cross(v1)
|
||||
local d1 = p1.x - p0.x
|
||||
local d2 = p1.y - p0.y
|
||||
local d3 = p1.z - p0.z
|
||||
local denom = Det3x3(v0.x,-v1.x,v2.x,v0.y,-v1.y,v2.y,v0.z,-v1.z,v2.z)
|
||||
|
||||
if (denom == 0) then
|
||||
return ZERO_VECTOR3 -- No solution (rays are parallel)
|
||||
end
|
||||
|
||||
local t0 = Det3x3(d1,-v1.x,v2.x,d2,-v1.y,v2.y,d3,-v1.z,v2.z) / denom
|
||||
local t1 = Det3x3(v0.x,d1,v2.x,v0.y,d2,v2.y,v0.z,d3,v2.z) / denom
|
||||
local s0 = p0 + t0 * v0
|
||||
local s1 = p1 + t1 * v1
|
||||
local s = s0 + 0.5 * ( s1 - s0 )
|
||||
|
||||
-- 0.25 studs is a threshold for deciding if the rays are
|
||||
-- close enough to be considered intersecting, found through testing
|
||||
if (s1-s0).Magnitude < 0.25 then
|
||||
return s
|
||||
else
|
||||
return ZERO_VECTOR3
|
||||
end
|
||||
end
|
||||
|
||||
local function SmartCircleBehavior(castPoints)
|
||||
local torsoUp = TorsoPart.CFrame.upVector.unit
|
||||
local torsoRight = TorsoPart.CFrame.rightVector.unit
|
||||
|
||||
-- SMART_CIRCLE mode includes rays to head and 5 to the torso.
|
||||
-- Hands, arms, legs and feet are not included since they
|
||||
-- are not canCollide and can therefore go inside of parts
|
||||
castPoints[#castPoints + 1] = TorsoPart.CFrame.p
|
||||
castPoints[#castPoints + 1] = TorsoPart.CFrame.p + torsoUp
|
||||
castPoints[#castPoints + 1] = TorsoPart.CFrame.p - torsoUp
|
||||
castPoints[#castPoints + 1] = TorsoPart.CFrame.p + torsoRight
|
||||
castPoints[#castPoints + 1] = TorsoPart.CFrame.p - torsoRight
|
||||
if HeadPart then
|
||||
castPoints[#castPoints + 1] = HeadPart.CFrame.p
|
||||
end
|
||||
|
||||
local cameraOrientation = Camera.CFrame - Camera.CFrame.p
|
||||
local torsoPoint = Vector3_new(0,0.5,0) + (TorsoPart and TorsoPart.Position or HumanoidRootPart.Position)
|
||||
local radius = 2.5
|
||||
|
||||
-- This loop first calculates points in a circle of radius 2.5 around the torso of the character, in the
|
||||
-- plane orthogonal to the camera's lookVector. Each point is then raycast to, to determine if it is within
|
||||
-- the free space surrounding the player (not inside anything). Two iterations are done to adjust points that
|
||||
-- are inside parts, to try to move them to valid locations that are still on their camera ray, so that the
|
||||
-- circle remains circular from the camera's perspective, but does not cast rays into walls or parts that are
|
||||
-- behind, below or beside the character and not really obstructing view of the character. This minimizes
|
||||
-- the undesirable situation where the character walks up to an exterior wall and it is made invisible even
|
||||
-- though it is behind the character.
|
||||
for i = 1, SMART_CIRCLE_CASTS do
|
||||
local angle = SMART_CIRCLE_INCREMENT * i - 0.5 * math.pi
|
||||
local offset = radius * Vector3_new(math_cos(angle), math_sin(angle), 0)
|
||||
local circlePoint = torsoPoint + cameraOrientation * offset
|
||||
|
||||
-- Vector from camera to point on the circle being tested
|
||||
local vp = circlePoint - Camera.CFrame.p
|
||||
|
||||
local ray = Ray.new(torsoPoint, circlePoint - torsoPoint)
|
||||
local hit, hp, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {Character}, false, false )
|
||||
local castPoint = circlePoint
|
||||
|
||||
if hit then
|
||||
local hprime = hp + 0.1 * hitNormal.unit -- Slightly offset hit point from the hit surface
|
||||
local v0 = hprime - torsoPoint -- Vector from torso to offset hit point
|
||||
local d0 = v0.magnitude
|
||||
|
||||
local perp = (v0:Cross(vp)).unit
|
||||
|
||||
-- Vector from the offset hit point, along the hit surface
|
||||
local v1 = (perp:Cross(hitNormal)).unit
|
||||
|
||||
-- Vector from camera to offset hit
|
||||
local vprime = (hprime - Camera.CFrame.p).unit
|
||||
|
||||
-- This dot product checks to see if the vector along the hit surface would hit the correct
|
||||
-- side of the invisicam cone, or if it would cross the camera look vector and hit the wrong side
|
||||
if ( v0.unit:Dot(-v1) < v0.unit:Dot(vprime)) then
|
||||
castPoint = RayIntersection(hprime, v1, circlePoint, vp)
|
||||
|
||||
if castPoint.Magnitude > 0 then
|
||||
local ray = Ray.new(hprime, castPoint - hprime)
|
||||
local hit, hitPoint, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {Character}, false, false )
|
||||
|
||||
if hit then
|
||||
local hprime2 = hitPoint + 0.1 * hitNormal.unit
|
||||
castPoint = hprime2
|
||||
end
|
||||
else
|
||||
castPoint = hprime
|
||||
end
|
||||
else
|
||||
castPoint = hprime
|
||||
end
|
||||
|
||||
local ray = Ray.new(torsoPoint, (castPoint - torsoPoint))
|
||||
local hit, hitPoint, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {Character}, false, false )
|
||||
|
||||
if hit then
|
||||
local castPoint2 = hitPoint - 0.1 * (castPoint - torsoPoint).unit
|
||||
castPoint = castPoint2
|
||||
end
|
||||
end
|
||||
|
||||
castPoints[#castPoints + 1] = castPoint
|
||||
end
|
||||
end
|
||||
|
||||
local function CheckTorsoReference()
|
||||
if Character then
|
||||
TorsoPart = Character:FindFirstChild("Torso")
|
||||
if not TorsoPart then
|
||||
TorsoPart = Character:FindFirstChild("UpperTorso")
|
||||
if not TorsoPart then
|
||||
TorsoPart = Character:FindFirstChild("HumanoidRootPart")
|
||||
end
|
||||
end
|
||||
|
||||
HeadPart = Character:FindFirstChild("Head")
|
||||
end
|
||||
end
|
||||
|
||||
local function OnCharacterAdded(character)
|
||||
if childAddedConn then
|
||||
childAddedConn:disconnect()
|
||||
childAddedConn = nil
|
||||
end
|
||||
if childRemovedConn then
|
||||
childRemovedConn:disconnect()
|
||||
childRemovedConn = nil
|
||||
end
|
||||
|
||||
Character = character
|
||||
|
||||
TrackedLimbs = {}
|
||||
local function childAdded(child)
|
||||
if child:IsA('BasePart') then
|
||||
if LIMB_TRACKING_SET[child.Name] then
|
||||
TrackedLimbs[child] = true
|
||||
end
|
||||
|
||||
if (child.Name == 'Torso' or child.Name == 'UpperTorso') then
|
||||
TorsoPart = child
|
||||
end
|
||||
|
||||
if (child.Name == 'Head') then
|
||||
HeadPart = child
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function childRemoved(child)
|
||||
TrackedLimbs[child] = nil
|
||||
|
||||
-- If removed/replaced part is 'Torso' or 'UpperTorso' double check that we still have a TorsoPart to use
|
||||
CheckTorsoReference()
|
||||
end
|
||||
|
||||
childAddedConn = character.ChildAdded:connect(childAdded)
|
||||
childRemovedConn = character.ChildRemoved:connect(childRemoved)
|
||||
|
||||
for _, child in pairs(Character:GetChildren()) do
|
||||
childAdded(child)
|
||||
end
|
||||
end
|
||||
|
||||
local function OnCurrentCameraChanged()
|
||||
local newCamera = workspace.CurrentCamera
|
||||
if newCamera then
|
||||
Camera = newCamera
|
||||
end
|
||||
end
|
||||
|
||||
-----------------------
|
||||
-- Exposed Functions --
|
||||
-----------------------
|
||||
|
||||
-- Update. Called every frame after the camera movement step
|
||||
function Invisicam:Update()
|
||||
|
||||
-- Bail if there is no Character
|
||||
if not Character then return end
|
||||
|
||||
-- Make sure we still have a HumanoidRootPart
|
||||
if not HumanoidRootPart then
|
||||
local humanoid = Character:FindFirstChildOfClass("Humanoid")
|
||||
if humanoid and humanoid.Torso then
|
||||
HumanoidRootPart = humanoid.Torso
|
||||
else
|
||||
-- Not set up with Humanoid? Try and see if there's one in the Character at all:
|
||||
HumanoidRootPart = Character:FindFirstChild("HumanoidRootPart")
|
||||
if not HumanoidRootPart then
|
||||
-- Bail out, since we're relying on HumanoidRootPart existing
|
||||
return
|
||||
end
|
||||
end
|
||||
local ancestryChangedConn;
|
||||
ancestryChangedConn = HumanoidRootPart.AncestryChanged:connect(function(child, parent)
|
||||
if child == HumanoidRootPart and not parent then
|
||||
HumanoidRootPart = nil
|
||||
if ancestryChangedConn and ancestryChangedConn.Connected then
|
||||
ancestryChangedConn:Disconnect()
|
||||
ancestryChangedConn = nil
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
if not TorsoPart then
|
||||
CheckTorsoReference()
|
||||
if not TorsoPart then
|
||||
-- Bail out, since we're relying on Torso existing, should never happen since we fall back to using HumanoidRootPart as torso
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Make a list of world points to raycast to
|
||||
local castPoints = {}
|
||||
BehaviorFunction(castPoints)
|
||||
|
||||
-- Cast to get a list of objects between the camera and the cast points
|
||||
local currentHits = {}
|
||||
local ignoreList = {Character}
|
||||
local function add(hit)
|
||||
currentHits[hit] = true
|
||||
if not SavedHits[hit] then
|
||||
SavedHits[hit] = hit.LocalTransparencyModifier
|
||||
end
|
||||
end
|
||||
|
||||
local hitParts
|
||||
local hitPartCount = 0
|
||||
|
||||
-- Hash table to treat head-ray-hit parts differently than the rest of the hit parts hit by other rays
|
||||
-- head/torso ray hit parts will be more transparent than peripheral parts when USE_STACKING_TRANSPARENCY is enabled
|
||||
local headTorsoRayHitParts = {}
|
||||
local partIsTouchingCamera = {}
|
||||
|
||||
local perPartTransparencyHeadTorsoHits = TARGET_TRANSPARENCY
|
||||
local perPartTransparencyOtherHits = TARGET_TRANSPARENCY
|
||||
|
||||
if USE_STACKING_TRANSPARENCY then
|
||||
|
||||
-- This first call uses head and torso rays to find out how many parts are stacked up
|
||||
-- for the purpose of calculating required per-part transparency
|
||||
local headPoint = HeadPart and HeadPart.CFrame.p or castPoints[1]
|
||||
local torsoPoint = TorsoPart and TorsoPart.CFrame.p or castPoints[2]
|
||||
hitParts = Camera:GetPartsObscuringTarget({headPoint, torsoPoint}, ignoreList)
|
||||
|
||||
-- Count how many things the sample rays passed through, including decals. This should only
|
||||
-- count decals facing the camera, but GetPartsObscuringTarget does not return surface normals,
|
||||
-- so my compromise for now is to just let any decal increase the part count by 1. Only one
|
||||
-- decal per part will be considered.
|
||||
for i = 1, #hitParts do
|
||||
local hitPart = hitParts[i]
|
||||
hitPartCount = hitPartCount + 1 -- count the part itself
|
||||
headTorsoRayHitParts[hitPart] = true
|
||||
for _, child in pairs(hitPart:GetChildren()) do
|
||||
if child:IsA('Decal') or child:IsA('Texture') then
|
||||
hitPartCount = hitPartCount + 1 -- count first decal hit, then break
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if (hitPartCount > 0) then
|
||||
perPartTransparencyHeadTorsoHits = math.pow( ((0.5 * TARGET_TRANSPARENCY) + (0.5 * TARGET_TRANSPARENCY / hitPartCount)), 1 / hitPartCount )
|
||||
perPartTransparencyOtherHits = math.pow( ((0.5 * TARGET_TRANSPARENCY_PERIPHERAL) + (0.5 * TARGET_TRANSPARENCY_PERIPHERAL / hitPartCount)), 1 / hitPartCount )
|
||||
end
|
||||
end
|
||||
|
||||
-- Now get all the parts hit by all the rays
|
||||
hitParts = Camera:GetPartsObscuringTarget(castPoints, ignoreList)
|
||||
|
||||
local partTargetTransparency = {}
|
||||
|
||||
-- Include decals and textures
|
||||
for i = 1, #hitParts do
|
||||
local hitPart = hitParts[i]
|
||||
|
||||
partTargetTransparency[hitPart] =headTorsoRayHitParts[hitPart] and perPartTransparencyHeadTorsoHits or perPartTransparencyOtherHits
|
||||
|
||||
-- If the part is not already as transparent or more transparent than what invisicam requires, add it to the list of
|
||||
-- parts to be modified by invisicam
|
||||
if hitPart.Transparency < partTargetTransparency[hitPart] then
|
||||
add(hitPart)
|
||||
end
|
||||
|
||||
-- Check all decals and textures on the part
|
||||
for _, child in pairs(hitPart:GetChildren()) do
|
||||
if child:IsA('Decal') or child:IsA('Texture') then
|
||||
if (child.Transparency < partTargetTransparency[hitPart]) then
|
||||
partTargetTransparency[child] = partTargetTransparency[hitPart]
|
||||
add(child)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Invisibilize objects that are in the way, restore those that aren't anymore
|
||||
for hitPart, originalLTM in pairs(SavedHits) do
|
||||
if currentHits[hitPart] then
|
||||
-- LocalTransparencyModifier gets whatever value is required to print the part's total transparency to equal perPartTransparency
|
||||
hitPart.LocalTransparencyModifier = (hitPart.Transparency < 1) and ((partTargetTransparency[hitPart] - hitPart.Transparency) / (1.0 - hitPart.Transparency)) or 0
|
||||
else -- Restore original pre-invisicam value of LTM
|
||||
hitPart.LocalTransparencyModifier = originalLTM
|
||||
SavedHits[hitPart] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Invisicam:SetMode(newMode)
|
||||
AssertTypes(newMode, 'number')
|
||||
for modeName, modeNum in pairs(MODE) do
|
||||
if modeNum == newMode then
|
||||
Mode = newMode
|
||||
BehaviorFunction = Behaviors[Mode]
|
||||
return
|
||||
end
|
||||
end
|
||||
error("Invalid mode number")
|
||||
end
|
||||
|
||||
function Invisicam:SetCustomBehavior(func)
|
||||
AssertTypes(func, 'function')
|
||||
Behaviors[MODE.CUSTOM] = func
|
||||
if Mode == MODE.CUSTOM then
|
||||
BehaviorFunction = func
|
||||
end
|
||||
end
|
||||
|
||||
function Invisicam:GetObscuredParts()
|
||||
return SavedHits
|
||||
end
|
||||
|
||||
-- Want to turn off Invisicam? Be sure to call this after.
|
||||
function Invisicam:Cleanup()
|
||||
for hit, originalFade in pairs(SavedHits) do
|
||||
hit.LocalTransparencyModifier = originalFade
|
||||
end
|
||||
end
|
||||
|
||||
---------------------
|
||||
--| Running Logic |--
|
||||
---------------------
|
||||
|
||||
-- Connect to the current and all future cameras
|
||||
workspace:GetPropertyChangedSignal("CurrentCamera"):connect(OnCurrentCameraChanged)
|
||||
OnCurrentCameraChanged()
|
||||
|
||||
Player.CharacterAdded:connect(OnCharacterAdded)
|
||||
if Player.Character then
|
||||
OnCharacterAdded(Player.Character)
|
||||
end
|
||||
|
||||
Behaviors[MODE.CUSTOM] = function() end -- (Does nothing until SetCustomBehavior)
|
||||
Behaviors[MODE.LIMBS] = LimbBehavior
|
||||
Behaviors[MODE.MOVEMENT] = MoveBehavior
|
||||
Behaviors[MODE.CORNERS] = CornerBehavior
|
||||
Behaviors[MODE.CIRCLE1] = CircleBehavior
|
||||
Behaviors[MODE.CIRCLE2] = CircleBehavior
|
||||
Behaviors[MODE.LIMBMOVE] = LimbMoveBehavior
|
||||
Behaviors[MODE.SMART_CIRCLE] = SmartCircleBehavior
|
||||
Behaviors[MODE.CHAR_OUTLINE] = CharacterOutlineBehavior
|
||||
|
||||
Invisicam:SetMode(STARTING_MODE)
|
||||
|
||||
return Invisicam
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
-- PopperCam Version 16
|
||||
-- OnlyTwentyCharacters
|
||||
|
||||
local PopperCam = {} -- Guarantees your players won't see outside the bounds of your map!
|
||||
|
||||
-----------------
|
||||
--| Constants |--
|
||||
-----------------
|
||||
|
||||
local POP_RESTORE_RATE = 0.3
|
||||
local MIN_CAMERA_ZOOM = 0.5
|
||||
|
||||
local VALID_SUBJECTS = {
|
||||
'Humanoid',
|
||||
'VehicleSeat',
|
||||
'SkateboardPlatform',
|
||||
}
|
||||
|
||||
local portraitPopperFixFlagExists, portraitPopperFixFlagEnabled = pcall(function()
|
||||
return UserSettings():IsUserFeatureEnabled("UserPortraitPopperFix")
|
||||
end)
|
||||
local FFlagUserPortraitPopperFix = portraitPopperFixFlagExists and portraitPopperFixFlagEnabled
|
||||
|
||||
-----------------
|
||||
--| Variables |--
|
||||
-----------------
|
||||
|
||||
local PlayersService = game:GetService('Players')
|
||||
|
||||
local Camera = nil
|
||||
local CameraSubjectChangeConn = nil
|
||||
|
||||
local SubjectPart = nil
|
||||
|
||||
local PlayerCharacters = {} -- For ignoring in raycasts
|
||||
local VehicleParts = {} -- Also just for ignoring
|
||||
|
||||
local LastPopAmount = 0
|
||||
local LastZoomLevel = 0
|
||||
local PopperEnabled = false
|
||||
|
||||
local CFrame_new = CFrame.new
|
||||
|
||||
-----------------------
|
||||
--| Local Functions |--
|
||||
-----------------------
|
||||
|
||||
local math_abs = math.abs
|
||||
|
||||
local function OnCameraSubjectChanged()
|
||||
VehicleParts = {}
|
||||
|
||||
local newSubject = Camera.CameraSubject
|
||||
if newSubject then
|
||||
-- Determine if we should be popping at all
|
||||
PopperEnabled = false
|
||||
for _, subjectType in pairs(VALID_SUBJECTS) do
|
||||
if newSubject:IsA(subjectType) then
|
||||
PopperEnabled = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Get all parts of the vehicle the player is controlling
|
||||
if newSubject:IsA('VehicleSeat') then
|
||||
VehicleParts = newSubject:GetConnectedParts(true)
|
||||
end
|
||||
|
||||
if FFlagUserPortraitPopperFix then
|
||||
if newSubject:IsA("BasePart") then
|
||||
SubjectPart = newSubject
|
||||
elseif newSubject:IsA("Model") then
|
||||
SubjectPart = newSubject.PrimaryPart
|
||||
elseif newSubject:IsA("Humanoid") then
|
||||
SubjectPart = newSubject.Torso
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function OnCharacterAdded(player, character)
|
||||
PlayerCharacters[player] = character
|
||||
end
|
||||
|
||||
local function OnPlayersChildAdded(child)
|
||||
if child:IsA('Player') then
|
||||
child.CharacterAdded:connect(function(character)
|
||||
OnCharacterAdded(child, character)
|
||||
end)
|
||||
if child.Character then
|
||||
OnCharacterAdded(child, child.Character)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function OnPlayersChildRemoved(child)
|
||||
if child:IsA('Player') then
|
||||
PlayerCharacters[child] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function OnWorkspaceChanged(property)
|
||||
if property == 'CurrentCamera' then
|
||||
local newCamera = workspace.CurrentCamera
|
||||
if newCamera then
|
||||
Camera = newCamera
|
||||
|
||||
if CameraSubjectChangeConn then
|
||||
CameraSubjectChangeConn:disconnect()
|
||||
end
|
||||
|
||||
CameraSubjectChangeConn = Camera:GetPropertyChangedSignal("CameraSubject"):connect(OnCameraSubjectChanged)
|
||||
OnCameraSubjectChanged()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------
|
||||
--| Exposed Functions |--
|
||||
-------------------------
|
||||
|
||||
function PopperCam:Update(EnabledCamera)
|
||||
if PopperEnabled then
|
||||
-- First, prep some intermediate vars
|
||||
local cameraCFrame = Camera.CFrame
|
||||
local focusPoint = Camera.Focus.p
|
||||
|
||||
if FFlagUserPortraitPopperFix and SubjectPart then
|
||||
focusPoint = SubjectPart.CFrame.p
|
||||
end
|
||||
|
||||
local ignoreList = {}
|
||||
for _, character in pairs(PlayerCharacters) do
|
||||
ignoreList[#ignoreList + 1] = character
|
||||
end
|
||||
for i = 1, #VehicleParts do
|
||||
ignoreList[#ignoreList + 1] = VehicleParts[i]
|
||||
end
|
||||
|
||||
-- Get largest cutoff distance
|
||||
local largest = Camera:GetLargestCutoffDistance(ignoreList)
|
||||
|
||||
-- Then check if the player zoomed since the last frame,
|
||||
-- and if so, reset our pop history so we stop tweening
|
||||
local zoomLevel = (cameraCFrame.p - focusPoint).Magnitude
|
||||
if math_abs(zoomLevel - LastZoomLevel) > 0.001 then
|
||||
LastPopAmount = 0
|
||||
end
|
||||
|
||||
-- Finally, zoom the camera in (pop) by that most-cut-off amount, or the last pop amount if that's more
|
||||
local popAmount = largest
|
||||
if LastPopAmount > popAmount then
|
||||
popAmount = LastPopAmount
|
||||
end
|
||||
|
||||
if popAmount > 0 then
|
||||
Camera.CFrame = cameraCFrame + (cameraCFrame.lookVector * popAmount)
|
||||
LastPopAmount = popAmount - POP_RESTORE_RATE -- Shrink it for the next frame
|
||||
if LastPopAmount < 0 then
|
||||
LastPopAmount = 0
|
||||
end
|
||||
end
|
||||
|
||||
LastZoomLevel = zoomLevel
|
||||
|
||||
-- Stop shift lock being able to see through walls by manipulating Camera focus inside the wall
|
||||
if EnabledCamera and EnabledCamera:GetShiftLock() and not EnabledCamera:IsInFirstPerson() then
|
||||
if EnabledCamera:GetCameraActualZoom() < 1 then
|
||||
local subjectPosition = EnabledCamera.lastSubjectPosition
|
||||
if subjectPosition then
|
||||
Camera.Focus = CFrame_new(subjectPosition)
|
||||
Camera.CFrame = CFrame_new(subjectPosition - MIN_CAMERA_ZOOM*EnabledCamera:GetCameraLook(), subjectPosition)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------
|
||||
--| Script Logic |--
|
||||
--------------------
|
||||
|
||||
-- Connect to the current and all future cameras
|
||||
workspace.Changed:connect(OnWorkspaceChanged)
|
||||
OnWorkspaceChanged('CurrentCamera')
|
||||
|
||||
-- Connect to all Players so we can ignore their Characters
|
||||
PlayersService.ChildRemoved:connect(OnPlayersChildRemoved)
|
||||
PlayersService.ChildAdded:connect(OnPlayersChildAdded)
|
||||
for _, player in pairs(PlayersService:GetPlayers()) do
|
||||
OnPlayersChildAdded(player)
|
||||
end
|
||||
|
||||
return PopperCam
|
||||
+1535
File diff suppressed because it is too large
Load Diff
+75
@@ -0,0 +1,75 @@
|
||||
local PlayersService = game:GetService('Players')
|
||||
local RootCameraCreator = require(script.Parent)
|
||||
|
||||
local ZERO_VECTOR2 = Vector2.new(0, 0)
|
||||
local XZ_VECTOR = Vector3.new(1,0,1)
|
||||
|
||||
local Vector2_new = Vector2.new
|
||||
local CFrame_new = CFrame.new
|
||||
local math_atan2 = math.atan2
|
||||
local math_min = math.min
|
||||
|
||||
local function IsFinite(num)
|
||||
return num == num and num ~= 1/0 and num ~= -1/0
|
||||
end
|
||||
|
||||
-- May return NaN or inf or -inf
|
||||
-- This is a way of finding the angle between the two vectors:
|
||||
local function findAngleBetweenXZVectors(vec2, vec1)
|
||||
return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z)
|
||||
end
|
||||
|
||||
local function CreateAttachCamera()
|
||||
local module = RootCameraCreator()
|
||||
|
||||
local lastUpdate = tick()
|
||||
function module:Update()
|
||||
local now = tick()
|
||||
|
||||
local camera = workspace.CurrentCamera
|
||||
|
||||
if lastUpdate == nil or now - lastUpdate > 1 then
|
||||
module:ResetCameraLook()
|
||||
self.LastCameraTransform = nil
|
||||
end
|
||||
|
||||
local subjectPosition = self:GetSubjectPosition()
|
||||
if subjectPosition and camera then
|
||||
local zoom = self:GetCameraZoom()
|
||||
if zoom <= 0 then
|
||||
zoom = 0.1
|
||||
end
|
||||
|
||||
|
||||
local humanoid = self:GetHumanoid()
|
||||
if lastUpdate and humanoid and humanoid.Torso then
|
||||
|
||||
-- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
|
||||
local delta = math_min(0.1, now - lastUpdate)
|
||||
local gamepadRotation = self:UpdateGamepad()
|
||||
self.RotateInput = self.RotateInput + (gamepadRotation * delta)
|
||||
|
||||
local forwardVector = humanoid.Torso.CFrame.lookVector
|
||||
|
||||
local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook())
|
||||
if IsFinite(y) then
|
||||
-- Preserve vertical rotation from user input
|
||||
self.RotateInput = Vector2_new(y, self.RotateInput.Y)
|
||||
end
|
||||
end
|
||||
|
||||
local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput)
|
||||
self.RotateInput = ZERO_VECTOR2
|
||||
|
||||
camera.Focus = CFrame_new(subjectPosition)
|
||||
local newCFrame = CFrame_new(subjectPosition - (zoom * newLookVector), subjectPosition)
|
||||
camera.CFrame = newCFrame
|
||||
self.LastCameraTransform = newCFrame
|
||||
end
|
||||
lastUpdate = now
|
||||
end
|
||||
|
||||
return module
|
||||
end
|
||||
|
||||
return CreateAttachCamera
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
|
||||
local PlayersService = game:GetService('Players')
|
||||
local VRService = game:GetService("VRService")
|
||||
|
||||
local RootCameraCreator = require(script.Parent)
|
||||
|
||||
local UP_VECTOR = Vector3.new(0, 1, 0)
|
||||
local XZ_VECTOR = Vector3.new(1, 0, 1)
|
||||
local ZERO_VECTOR2 = Vector2.new(0, 0)
|
||||
|
||||
local VR_PITCH_FRACTION = 0.25
|
||||
|
||||
local Vector3_new = Vector3.new
|
||||
local CFrame_new = CFrame.new
|
||||
local math_min = math.min
|
||||
local math_max = math.max
|
||||
local math_atan2 = math.atan2
|
||||
local math_rad = math.rad
|
||||
local math_abs = math.abs
|
||||
|
||||
local function clamp(low, high, num)
|
||||
return (num > high and high or num < low and low or num)
|
||||
end
|
||||
|
||||
local function IsFinite(num)
|
||||
return num == num and num ~= 1/0 and num ~= -1/0
|
||||
end
|
||||
|
||||
local function IsFiniteVector3(vec3)
|
||||
return IsFinite(vec3.x) and IsFinite(vec3.y) and IsFinite(vec3.z)
|
||||
end
|
||||
|
||||
-- May return NaN or inf or -inf
|
||||
-- This is a way of finding the angle between the two vectors:
|
||||
local function findAngleBetweenXZVectors(vec2, vec1)
|
||||
return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z)
|
||||
end
|
||||
|
||||
local function CreateClassicCamera()
|
||||
local module = RootCameraCreator()
|
||||
|
||||
local tweenAcceleration = math_rad(220)
|
||||
local tweenSpeed = math_rad(0)
|
||||
local tweenMaxSpeed = math_rad(250)
|
||||
local timeBeforeAutoRotate = 2
|
||||
|
||||
local lastUpdate = tick()
|
||||
module.LastUserPanCamera = tick()
|
||||
function module:Update()
|
||||
module:ProcessTweens()
|
||||
local now = tick()
|
||||
local timeDelta = (now - lastUpdate)
|
||||
|
||||
local userPanningTheCamera = (self.UserPanningTheCamera == true)
|
||||
local camera = workspace.CurrentCamera
|
||||
local player = PlayersService.LocalPlayer
|
||||
local humanoid = self:GetHumanoid()
|
||||
local cameraSubject = camera and camera.CameraSubject
|
||||
local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
|
||||
local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
|
||||
|
||||
if lastUpdate == nil or now - lastUpdate > 1 then
|
||||
module:ResetCameraLook()
|
||||
self.LastCameraTransform = nil
|
||||
end
|
||||
|
||||
if lastUpdate then
|
||||
local gamepadRotation = self:UpdateGamepad()
|
||||
|
||||
if self:ShouldUseVRRotation() then
|
||||
self.RotateInput = self.RotateInput + self:GetVRRotationInput()
|
||||
else
|
||||
-- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
|
||||
local delta = math_min(0.1, now - lastUpdate)
|
||||
|
||||
if gamepadRotation ~= ZERO_VECTOR2 then
|
||||
userPanningTheCamera = true
|
||||
self.RotateInput = self.RotateInput + (gamepadRotation * delta)
|
||||
end
|
||||
|
||||
local angle = 0
|
||||
if not (isInVehicle or isOnASkateboard) then
|
||||
angle = angle + (self.TurningLeft and -120 or 0)
|
||||
angle = angle + (self.TurningRight and 120 or 0)
|
||||
end
|
||||
|
||||
if angle ~= 0 then
|
||||
self.RotateInput = self.RotateInput + Vector2.new(math_rad(angle * delta), 0)
|
||||
userPanningTheCamera = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Reset tween speed if user is panning
|
||||
if userPanningTheCamera then
|
||||
tweenSpeed = 0
|
||||
module.LastUserPanCamera = tick()
|
||||
end
|
||||
|
||||
local userRecentlyPannedCamera = now - module.LastUserPanCamera < timeBeforeAutoRotate
|
||||
local subjectPosition = self:GetSubjectPosition()
|
||||
|
||||
if subjectPosition and player and camera then
|
||||
local zoom = self:GetCameraZoom()
|
||||
if zoom < 0.5 then
|
||||
zoom = 0.5
|
||||
end
|
||||
|
||||
if self:GetShiftLock() and not self:IsInFirstPerson() then
|
||||
-- We need to use the right vector of the camera after rotation, not before
|
||||
local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput)
|
||||
local offset = ((newLookVector * XZ_VECTOR):Cross(UP_VECTOR).unit * 1.75)
|
||||
|
||||
if IsFiniteVector3(offset) then
|
||||
subjectPosition = subjectPosition + offset
|
||||
end
|
||||
else
|
||||
if not userPanningTheCamera and self.LastCameraTransform then
|
||||
local isInFirstPerson = self:IsInFirstPerson()
|
||||
if (isInVehicle or isOnASkateboard) and lastUpdate and humanoid and humanoid.Torso then
|
||||
if isInFirstPerson then
|
||||
if self.LastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
|
||||
local y = -findAngleBetweenXZVectors(self.LastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector)
|
||||
if IsFinite(y) then
|
||||
self.RotateInput = self.RotateInput + Vector2.new(y, 0)
|
||||
end
|
||||
tweenSpeed = 0
|
||||
end
|
||||
elseif not userRecentlyPannedCamera then
|
||||
local forwardVector = humanoid.Torso.CFrame.lookVector
|
||||
if isOnASkateboard then
|
||||
forwardVector = cameraSubject.CFrame.lookVector
|
||||
end
|
||||
|
||||
tweenSpeed = clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta)
|
||||
|
||||
local percent = clamp(0, 1, tweenSpeed * timeDelta)
|
||||
if self:IsInFirstPerson() then
|
||||
percent = 1
|
||||
end
|
||||
|
||||
local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook())
|
||||
if IsFinite(y) and math_abs(y) > 0.0001 then
|
||||
self.RotateInput = self.RotateInput + Vector2.new(y * percent, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local VREnabled = VRService.VREnabled
|
||||
camera.Focus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame_new(subjectPosition)
|
||||
|
||||
local cameraFocusP = camera.Focus.p
|
||||
if VREnabled and not self:IsInFirstPerson() then
|
||||
local cameraHeight = self:GetCameraHeight()
|
||||
local vecToSubject = (subjectPosition - camera.CFrame.p)
|
||||
local distToSubject = vecToSubject.magnitude
|
||||
|
||||
-- Only move the camera if it exceeded a maximum distance to the subject in VR
|
||||
if distToSubject > zoom or self.RotateInput.x ~= 0 then
|
||||
local desiredDist = math_min(distToSubject, zoom)
|
||||
vecToSubject = self:RotateCamera(vecToSubject.unit * XZ_VECTOR, Vector2.new(self.RotateInput.x, 0)) * desiredDist
|
||||
local newPos = cameraFocusP - vecToSubject
|
||||
local desiredLookDir = camera.CFrame.lookVector
|
||||
if self.RotateInput.x ~= 0 then
|
||||
desiredLookDir = vecToSubject
|
||||
end
|
||||
local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z)
|
||||
self.RotateInput = ZERO_VECTOR2
|
||||
|
||||
camera.CFrame = CFrame_new(newPos, lookAt) + Vector3_new(0, cameraHeight, 0)
|
||||
end
|
||||
else
|
||||
local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput)
|
||||
self.RotateInput = ZERO_VECTOR2
|
||||
camera.CFrame = CFrame_new(cameraFocusP - (zoom * newLookVector), cameraFocusP)
|
||||
end
|
||||
|
||||
self.LastCameraTransform = camera.CFrame
|
||||
self.LastCameraFocus = camera.Focus
|
||||
if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
|
||||
self.LastSubjectCFrame = cameraSubject.CFrame
|
||||
else
|
||||
self.LastSubjectCFrame = nil
|
||||
end
|
||||
end
|
||||
|
||||
lastUpdate = now
|
||||
end
|
||||
|
||||
return module
|
||||
end
|
||||
|
||||
return CreateClassicCamera
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
local PlayersService = game:GetService('Players')
|
||||
local RootCameraCreator = require(script.Parent)
|
||||
|
||||
local ZERO_VECTOR2 = Vector2.new(0, 0)
|
||||
|
||||
local CFrame_new = CFrame.new
|
||||
local math_min = math.min
|
||||
|
||||
local function CreateFixedCamera()
|
||||
local module = RootCameraCreator()
|
||||
|
||||
local lastUpdate = tick()
|
||||
function module:Update()
|
||||
local now = tick()
|
||||
|
||||
local camera = workspace.CurrentCamera
|
||||
local player = PlayersService.LocalPlayer
|
||||
if lastUpdate == nil or now - lastUpdate > 1 then
|
||||
module:ResetCameraLook()
|
||||
self.LastCameraTransform = nil
|
||||
end
|
||||
|
||||
if lastUpdate then
|
||||
-- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
|
||||
local delta = math_min(0.1, now - lastUpdate)
|
||||
local gamepadRotation = self:UpdateGamepad()
|
||||
self.RotateInput = self.RotateInput + (gamepadRotation * delta)
|
||||
end
|
||||
|
||||
local subjectPosition = self:GetSubjectPosition()
|
||||
if subjectPosition and player and camera then
|
||||
local zoom = self:GetCameraZoom()
|
||||
if zoom <= 0 then
|
||||
zoom = 0.1
|
||||
end
|
||||
local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput)
|
||||
self.RotateInput = ZERO_VECTOR2
|
||||
|
||||
camera.CFrame = CFrame_new(camera.CFrame.p, camera.CFrame.p + (zoom * newLookVector))
|
||||
self.LastCameraTransform = camera.CFrame
|
||||
end
|
||||
lastUpdate = now
|
||||
end
|
||||
|
||||
return module
|
||||
end
|
||||
|
||||
return CreateFixedCamera
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
local PlayersService = game:GetService('Players')
|
||||
local VRService = game:GetService("VRService")
|
||||
|
||||
local RootCameraCreator = require(script.Parent)
|
||||
|
||||
local CFrame_new = CFrame.new
|
||||
local Vector2_new = Vector2.new
|
||||
local Vector3_new = Vector3.new
|
||||
local math_min = math.min
|
||||
local math_max = math.max
|
||||
local math_atan2 = math.atan2
|
||||
local math_rad = math.rad
|
||||
local math_abs = math.abs
|
||||
|
||||
local HUMANOIDSTATE_CLIMBING = Enum.HumanoidStateType.Climbing
|
||||
local ZERO_VECTOR2 = Vector2.new(0, 0)
|
||||
local UP_VECTOR = Vector3.new(0, 1, 0)
|
||||
local XZ_VECTOR = Vector3.new(1, 0, 1)
|
||||
local ZERO_VECTOR3 = Vector3.new(0, 0, 0)
|
||||
local PORTRAIT_OFFSET = Vector3.new(0, -3, 0)
|
||||
|
||||
local function clamp(low, high, num)
|
||||
return num > high and high or num < low and low or num
|
||||
end
|
||||
|
||||
local function IsFinite(num)
|
||||
return num == num and num ~= 1/0 and num ~= -1/0
|
||||
end
|
||||
|
||||
local function IsFiniteVector3(vec3)
|
||||
return IsFinite(vec3.x) and IsFinite(vec3.y) and IsFinite(vec3.z)
|
||||
end
|
||||
|
||||
-- May return NaN or inf or -inf
|
||||
local function findAngleBetweenXZVectors(vec2, vec1)
|
||||
-- This is a way of finding the angle between the two vectors:
|
||||
return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z)
|
||||
end
|
||||
|
||||
local function CreateFollowCamera()
|
||||
local module = RootCameraCreator()
|
||||
|
||||
local tweenAcceleration = math_rad(220)
|
||||
local tweenSpeed = math_rad(0)
|
||||
local tweenMaxSpeed = math_rad(250)
|
||||
local timeBeforeAutoRotate = 2
|
||||
|
||||
local lastUpdate = tick()
|
||||
module.LastUserPanCamera = tick()
|
||||
function module:Update()
|
||||
module:ProcessTweens()
|
||||
local now = tick()
|
||||
local timeDelta = (now - lastUpdate)
|
||||
|
||||
local userPanningTheCamera = (self.UserPanningTheCamera == true)
|
||||
local camera = workspace.CurrentCamera
|
||||
local player = PlayersService.LocalPlayer
|
||||
local humanoid = self:GetHumanoid()
|
||||
local cameraSubject = camera and camera.CameraSubject
|
||||
local isClimbing = humanoid and humanoid:GetState() == HUMANOIDSTATE_CLIMBING
|
||||
local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
|
||||
local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
|
||||
|
||||
if lastUpdate == nil or now - lastUpdate > 1 then
|
||||
module:ResetCameraLook()
|
||||
self.LastCameraTransform = nil
|
||||
end
|
||||
|
||||
if lastUpdate then
|
||||
|
||||
if self:ShouldUseVRRotation() then
|
||||
self.RotateInput = self.RotateInput + self:GetVRRotationInput()
|
||||
else
|
||||
-- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
|
||||
local delta = math_min(0.1, now - lastUpdate)
|
||||
local angle = 0
|
||||
-- NOTE: Traditional follow camera does not rotate with arrow keys
|
||||
if not (isInVehicle or isOnASkateboard) then
|
||||
angle = angle + (self.TurningLeft and -120 or 0)
|
||||
angle = angle + (self.TurningRight and 120 or 0)
|
||||
end
|
||||
|
||||
local gamepadRotation = self:UpdateGamepad()
|
||||
if gamepadRotation ~= Vector2.new(0,0) then
|
||||
userPanningTheCamera = true
|
||||
self.RotateInput = self.RotateInput + (gamepadRotation * delta)
|
||||
end
|
||||
|
||||
if angle ~= 0 then
|
||||
userPanningTheCamera = true
|
||||
self.RotateInput = self.RotateInput + Vector2_new(math_rad(angle * delta), 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Reset tween speed if user is panning
|
||||
if userPanningTheCamera then
|
||||
tweenSpeed = 0
|
||||
module.LastUserPanCamera = tick()
|
||||
end
|
||||
|
||||
local userRecentlyPannedCamera = now - module.LastUserPanCamera < timeBeforeAutoRotate
|
||||
|
||||
local subjectPosition = self:GetSubjectPosition()
|
||||
if subjectPosition and player and camera then
|
||||
local zoom = self:GetCameraZoom()
|
||||
if zoom < 0.5 then
|
||||
zoom = 0.5
|
||||
end
|
||||
|
||||
if self:GetShiftLock() and not self:IsInFirstPerson() then
|
||||
local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput)
|
||||
local offset = ((newLookVector * XZ_VECTOR):Cross(UP_VECTOR).unit * 1.75)
|
||||
if IsFiniteVector3(offset) then
|
||||
subjectPosition = subjectPosition + offset
|
||||
end
|
||||
else
|
||||
if self.LastCameraTransform and not userPanningTheCamera then
|
||||
local isInFirstPerson = self:IsInFirstPerson()
|
||||
if (isClimbing or isInVehicle or isOnASkateboard) and lastUpdate and humanoid and humanoid.Torso then
|
||||
if isInFirstPerson then
|
||||
if self.LastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
|
||||
local y = -findAngleBetweenXZVectors(self.LastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector)
|
||||
if IsFinite(y) then
|
||||
self.RotateInput = self.RotateInput + Vector2.new(y, 0)
|
||||
end
|
||||
tweenSpeed = 0
|
||||
end
|
||||
elseif not userRecentlyPannedCamera then
|
||||
local forwardVector = humanoid.Torso.CFrame.lookVector
|
||||
if isOnASkateboard then
|
||||
forwardVector = cameraSubject.CFrame.lookVector
|
||||
end
|
||||
|
||||
tweenSpeed = clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta)
|
||||
|
||||
local percent = clamp(0, 1, tweenSpeed*timeDelta)
|
||||
if not isClimbing and self:IsInFirstPerson() then
|
||||
percent = 1
|
||||
end
|
||||
local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook())
|
||||
-- Check for NaN
|
||||
if IsFinite(y) and math_abs(y) > 0.0001 then
|
||||
self.RotateInput = self.RotateInput + Vector2.new(y * percent, 0)
|
||||
end
|
||||
end
|
||||
elseif not (isInFirstPerson or userRecentlyPannedCamera) and not VRService.VREnabled then
|
||||
local lastVec = -(self.LastCameraTransform.p - subjectPosition)
|
||||
|
||||
local y = findAngleBetweenXZVectors(lastVec, self:GetCameraLook())
|
||||
|
||||
-- This cutoff is to decide if the humanoid's angle of movement,
|
||||
-- relative to the camera's look vector, is enough that
|
||||
-- we want the camera to be following them. The point is to provide
|
||||
-- a sizable deadzone to allow more precise forward movements.
|
||||
local thetaCutoff = 0.4
|
||||
|
||||
-- Check for NaNs
|
||||
if IsFinite(y) and math.abs(y) > 0.0001 and math_abs(y) > thetaCutoff*timeDelta then
|
||||
self.RotateInput = self.RotateInput + Vector2.new(y, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput)
|
||||
self.RotateInput = ZERO_VECTOR2
|
||||
|
||||
if VRService.VREnabled then
|
||||
camera.Focus = self:GetVRFocus(subjectPosition, timeDelta)
|
||||
elseif self:IsPortraitMode() then
|
||||
camera.Focus = CFrame_new(subjectPosition + PORTRAIT_OFFSET)
|
||||
else
|
||||
camera.Focus = CFrame_new(subjectPosition)
|
||||
end
|
||||
camera.CFrame = CFrame_new(camera.Focus.p - (zoom * newLookVector), camera.Focus.p) + Vector3.new(0, self:GetCameraHeight(), 0)
|
||||
|
||||
self.LastCameraTransform = camera.CFrame
|
||||
self.LastCameraFocus = camera.Focus
|
||||
if isInVehicle or isOnASkateboard and cameraSubject:IsA('BasePart') then
|
||||
self.LastSubjectCFrame = cameraSubject.CFrame
|
||||
else
|
||||
self.LastSubjectCFrame = nil
|
||||
end
|
||||
end
|
||||
|
||||
lastUpdate = now
|
||||
end
|
||||
|
||||
return module
|
||||
end
|
||||
|
||||
return CreateFollowCamera
|
||||
+450
@@ -0,0 +1,450 @@
|
||||
--[[
|
||||
Orbital Camera 1.0.0
|
||||
AllYourBlox
|
||||
|
||||
Derived from ClassicCamera, adds camera angle constraints, and represents position values in spherical
|
||||
coordinates (azimuth, elevation, radius), instead of Cartesian coordinates (x, y, z). Azimuth is the
|
||||
angle of rotation about the Y axis, with the zero-angle reference point corresponding to offsetting
|
||||
the camera in the +Z direction, from where it will be looking in the -Z direction by default. Elevation
|
||||
is the angle up from the XZ plane, where zero degrees is on the plane, and +90 degrees is on the +Y
|
||||
axis. Distance is the camera-to-subject distance, the spherical coordinates radius, specified in studs.
|
||||
--]]
|
||||
|
||||
-- Do not edit these values, they are not the developer-set limits, they are limits
|
||||
-- to the values the camera system equations can correctly handle
|
||||
local MIN_ALLOWED_ELEVATION_DEG = -80
|
||||
local MAX_ALLOWED_ELEVATION_DEG = 80
|
||||
|
||||
local externalProperties = {}
|
||||
externalProperties["InitialDistance"] = 25
|
||||
externalProperties["MinDistance"] = 10
|
||||
externalProperties["MaxDistance"] = 100
|
||||
externalProperties["InitialElevation"] = 35
|
||||
externalProperties["MinElevation"] = 35
|
||||
externalProperties["MaxElevation"] = 35
|
||||
externalProperties["ReferenceAzimuth"] = -45 -- Angle around the Y axis where the camera starts. -45 offsets the camera in the -X and +Z directions equally
|
||||
externalProperties["CWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CW as seen from above
|
||||
externalProperties["CCWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CCW as seen from above
|
||||
externalProperties["UseAzimuthLimits"] = false -- Full rotation around Y axis available by default
|
||||
|
||||
local refAzimuthRad
|
||||
local curAzimuthRad
|
||||
local minAzimuthAbsoluteRad
|
||||
local maxAzimuthAbsoluteRad
|
||||
local useAzimuthLimits
|
||||
local curElevationRad
|
||||
local minElevationRad
|
||||
local maxElevationRad
|
||||
local curDistance
|
||||
local minDistance
|
||||
local maxDistance
|
||||
|
||||
local UNIT_Z = Vector3.new(0,0,1)
|
||||
local TAU = 2 * math.pi
|
||||
|
||||
local changedSignalConnections = {}
|
||||
|
||||
-- End of OrbitalCamera additions
|
||||
local PlayersService = game:GetService('Players')
|
||||
local VRService = game:GetService("VRService")
|
||||
|
||||
local RootCameraCreator = require(script.Parent)
|
||||
|
||||
local UP_VECTOR = Vector3.new(0, 1, 0)
|
||||
local XZ_VECTOR = Vector3.new(1, 0, 1)
|
||||
local ZERO_VECTOR2 = Vector2.new(0, 0)
|
||||
|
||||
local VR_PITCH_FRACTION = 0.25
|
||||
|
||||
local Vector3_new = Vector3.new
|
||||
local CFrame_new = CFrame.new
|
||||
local math_min = math.min
|
||||
local math_max = math.max
|
||||
local math_atan2 = math.atan2
|
||||
local math_rad = math.rad
|
||||
local math_abs = math.abs
|
||||
|
||||
--[[ Gamepad Support ]]--
|
||||
local THUMBSTICK_DEADZONE = 0.2
|
||||
local r3ButtonDown = false
|
||||
local l3ButtonDown = false
|
||||
local currentZoomSpeed = 1 -- Multiplier, so 1 == no zooming
|
||||
|
||||
local function clamp(value, minValue, maxValue)
|
||||
if maxValue < minValue then
|
||||
maxValue = minValue
|
||||
end
|
||||
return math.clamp(value, minValue, maxValue)
|
||||
end
|
||||
|
||||
local function IsFinite(num)
|
||||
return num == num and num ~= 1/0 and num ~= -1/0
|
||||
end
|
||||
|
||||
local function IsFiniteVector3(vec3)
|
||||
return IsFinite(vec3.x) and IsFinite(vec3.y) and IsFinite(vec3.z)
|
||||
end
|
||||
|
||||
-- May return NaN or inf or -inf
|
||||
-- This is a way of finding the angle between the two vectors:
|
||||
local function findAngleBetweenXZVectors(vec2, vec1)
|
||||
return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z)
|
||||
end
|
||||
|
||||
local function GetValueObject(name, defaultValue)
|
||||
local valueObj = script:FindFirstChild(name)
|
||||
if valueObj then
|
||||
return valueObj.Value
|
||||
end
|
||||
return defaultValue
|
||||
end
|
||||
|
||||
local function LoadOrCreateNumberValueParameter(name, valueType, updateFunction)
|
||||
local valueObj = script:FindFirstChild(name)
|
||||
|
||||
if valueObj and valueObj:isA(valueType) then
|
||||
-- Value object exists and is the correct type, use its value
|
||||
externalProperties[name] = valueObj.Value
|
||||
elseif externalProperties[name] ~= nil then
|
||||
-- Create missing (or replace incorrectly-typed) valueObject with default value
|
||||
valueObj = Instance.new(valueType)
|
||||
valueObj.Name = name
|
||||
valueObj.Parent = script
|
||||
valueObj.Value = externalProperties[name]
|
||||
else
|
||||
print("externalProperties table has no entry for ",name)
|
||||
return
|
||||
end
|
||||
|
||||
if updateFunction then
|
||||
if changedSignalConnections[name] then
|
||||
changedSignalConnections[name]:disconnect()
|
||||
end
|
||||
changedSignalConnections[name] = valueObj.Changed:connect(function(newValue)
|
||||
externalProperties[name] = newValue
|
||||
updateFunction()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
local function SetAndBoundsCheckAzimuthValues()
|
||||
minAzimuthAbsoluteRad = math.rad(externalProperties["ReferenceAzimuth"]) - math.abs(math.rad(externalProperties["CWAzimuthTravel"]))
|
||||
maxAzimuthAbsoluteRad = math.rad(externalProperties["ReferenceAzimuth"]) + math.abs(math.rad(externalProperties["CCWAzimuthTravel"]))
|
||||
useAzimuthLimits = externalProperties["UseAzimuthLimits"]
|
||||
if useAzimuthLimits then
|
||||
curAzimuthRad = math.max(curAzimuthRad, minAzimuthAbsoluteRad)
|
||||
curAzimuthRad = math.min(curAzimuthRad, maxAzimuthAbsoluteRad)
|
||||
end
|
||||
end
|
||||
|
||||
local function SetAndBoundsCheckElevationValues()
|
||||
-- These degree values are the direct user input values. It is deliberate that they are
|
||||
-- ranged checked only against the extremes, and not against each other. Any time one
|
||||
-- is changed, both of the internal values in radians are recalculated. This allows for
|
||||
-- A developer to change the values in any order and for the end results to be that the
|
||||
-- internal values adjust to match intent as best as possible.
|
||||
local minElevationDeg = math.max(externalProperties["MinElevation"], MIN_ALLOWED_ELEVATION_DEG)
|
||||
local maxElevationDeg = math.min(externalProperties["MaxElevation"], MAX_ALLOWED_ELEVATION_DEG)
|
||||
|
||||
-- Set internal values in radians
|
||||
minElevationRad = math.rad(math.min(minElevationDeg, maxElevationDeg))
|
||||
maxElevationRad = math.rad(math.max(minElevationDeg, maxElevationDeg))
|
||||
curElevationRad = math.max(curElevationRad, minElevationRad)
|
||||
curElevationRad = math.min(curElevationRad, maxElevationRad)
|
||||
end
|
||||
|
||||
local function SetAndBoundsCheckDistanceValues()
|
||||
minDistance = externalProperties["MinDistance"]
|
||||
maxDistance = externalProperties["MaxDistance"]
|
||||
curDistance = math.max(curDistance, minDistance)
|
||||
curDistance = math.min(curDistance, maxDistance)
|
||||
end
|
||||
|
||||
-- This loads from, or lazily creates, NumberValue objects for exposed parameters
|
||||
local function LoadNumberValueParameters()
|
||||
-- These initial values do not require change listeners since they are read only once
|
||||
LoadOrCreateNumberValueParameter("InitialElevation", "NumberValue", nil)
|
||||
LoadOrCreateNumberValueParameter("InitialDistance", "NumberValue", nil)
|
||||
|
||||
-- Note: ReferenceAzimuth is also used as an initial value, but needs a change listener because it is used in the calculation of the limits
|
||||
LoadOrCreateNumberValueParameter("ReferenceAzimuth", "NumberValue", SetAndBoundsCheckAzimuthValues)
|
||||
LoadOrCreateNumberValueParameter("CWAzimuthTravel", "NumberValue", SetAndBoundsCheckAzimuthValues)
|
||||
LoadOrCreateNumberValueParameter("CCWAzimuthTravel", "NumberValue", SetAndBoundsCheckAzimuthValues)
|
||||
LoadOrCreateNumberValueParameter("MinElevation", "NumberValue", SetAndBoundsCheckElevationValues)
|
||||
LoadOrCreateNumberValueParameter("MaxElevation", "NumberValue", SetAndBoundsCheckElevationValues)
|
||||
LoadOrCreateNumberValueParameter("MinDistance", "NumberValue", SetAndBoundsCheckDistanceValues)
|
||||
LoadOrCreateNumberValueParameter("MaxDistance", "NumberValue", SetAndBoundsCheckDistanceValues)
|
||||
LoadOrCreateNumberValueParameter("UseAzimuthLimits", "BoolValue", SetAndBoundsCheckAzimuthValues)
|
||||
|
||||
-- Internal values set (in radians, from degrees), plus sanitization
|
||||
curAzimuthRad = math.rad(externalProperties["ReferenceAzimuth"])
|
||||
curElevationRad = math.rad(externalProperties["InitialElevation"])
|
||||
curDistance = externalProperties["InitialDistance"]
|
||||
|
||||
SetAndBoundsCheckAzimuthValues()
|
||||
SetAndBoundsCheckElevationValues()
|
||||
SetAndBoundsCheckDistanceValues()
|
||||
end
|
||||
|
||||
local function CreateOrbitalCamera()
|
||||
local module = RootCameraCreator()
|
||||
|
||||
LoadNumberValueParameters()
|
||||
|
||||
module.DefaultZoom = curDistance
|
||||
|
||||
local tweenAcceleration = math_rad(220)
|
||||
local tweenSpeed = math_rad(0)
|
||||
local tweenMaxSpeed = math_rad(250)
|
||||
local timeBeforeAutoRotate = 2
|
||||
|
||||
local lastUpdate = tick()
|
||||
module.LastUserPanCamera = tick()
|
||||
|
||||
function module:Update()
|
||||
module:ProcessTweens()
|
||||
local now = tick()
|
||||
local timeDelta = (now - lastUpdate)
|
||||
local userPanningTheCamera = (self.UserPanningTheCamera == true)
|
||||
local camera = workspace.CurrentCamera
|
||||
local player = PlayersService.LocalPlayer
|
||||
local humanoid = self:GetHumanoid()
|
||||
local cameraSubject = camera and camera.CameraSubject
|
||||
local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
|
||||
local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
|
||||
|
||||
if lastUpdate == nil or now - lastUpdate > 1 then
|
||||
module:ResetCameraLook()
|
||||
self.LastCameraTransform = nil
|
||||
end
|
||||
|
||||
if lastUpdate then
|
||||
local gamepadRotation = self:UpdateGamepad()
|
||||
|
||||
if self:ShouldUseVRRotation() then
|
||||
self.RotateInput = self.RotateInput + self:GetVRRotationInput()
|
||||
else
|
||||
-- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
|
||||
local delta = math_min(0.1, now - lastUpdate)
|
||||
|
||||
if gamepadRotation ~= ZERO_VECTOR2 then
|
||||
userPanningTheCamera = true
|
||||
self.RotateInput = self.RotateInput + (gamepadRotation * delta)
|
||||
end
|
||||
|
||||
local angle = 0
|
||||
if not (isInVehicle or isOnASkateboard) then
|
||||
angle = angle + (self.TurningLeft and -120 or 0)
|
||||
angle = angle + (self.TurningRight and 120 or 0)
|
||||
end
|
||||
|
||||
if angle ~= 0 then
|
||||
self.RotateInput = self.RotateInput + Vector2.new(math_rad(angle * delta), 0)
|
||||
userPanningTheCamera = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Reset tween speed if user is panning
|
||||
if userPanningTheCamera then
|
||||
tweenSpeed = 0
|
||||
module.LastUserPanCamera = tick()
|
||||
end
|
||||
|
||||
local userRecentlyPannedCamera = now - module.LastUserPanCamera < timeBeforeAutoRotate
|
||||
local subjectPosition = self:GetSubjectPosition()
|
||||
|
||||
if subjectPosition and player and camera then
|
||||
|
||||
|
||||
local zoom = self:ZoomCamera(curDistance * currentZoomSpeed)
|
||||
|
||||
|
||||
if not userPanningTheCamera and self.LastCameraTransform then
|
||||
local isInFirstPerson = self:IsInFirstPerson()
|
||||
if (isInVehicle or isOnASkateboard) and lastUpdate and humanoid and humanoid.Torso then
|
||||
if isInFirstPerson then
|
||||
if self.LastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
|
||||
local y = -findAngleBetweenXZVectors(self.LastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector)
|
||||
if IsFinite(y) then
|
||||
self.RotateInput = self.RotateInput + Vector2.new(y, 0)
|
||||
end
|
||||
tweenSpeed = 0
|
||||
end
|
||||
elseif not userRecentlyPannedCamera then
|
||||
local forwardVector = humanoid.Torso.CFrame.lookVector
|
||||
if isOnASkateboard then
|
||||
forwardVector = cameraSubject.CFrame.lookVector
|
||||
end
|
||||
|
||||
tweenSpeed = clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta)
|
||||
|
||||
local percent = clamp(0, 1, tweenSpeed * timeDelta)
|
||||
if self:IsInFirstPerson() then
|
||||
percent = 1
|
||||
end
|
||||
|
||||
local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook())
|
||||
if IsFinite(y) and math_abs(y) > 0.0001 then
|
||||
self.RotateInput = self.RotateInput + Vector2.new(y * percent, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local VREnabled = VRService.VREnabled
|
||||
camera.Focus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame_new(subjectPosition)
|
||||
|
||||
local cameraFocusP = camera.Focus.p
|
||||
if VREnabled and not self:IsInFirstPerson() then
|
||||
local cameraHeight = self:GetCameraHeight()
|
||||
local vecToSubject = (subjectPosition - camera.CFrame.p)
|
||||
local distToSubject = vecToSubject.magnitude
|
||||
|
||||
-- Only move the camera if it exceeded a maximum distance to the subject in VR
|
||||
if distToSubject > zoom or self.RotateInput.x ~= 0 then
|
||||
local desiredDist = math_min(distToSubject, zoom)
|
||||
vecToSubject = self:RotateCamera(vecToSubject.unit * XZ_VECTOR, Vector2.new(self.RotateInput.x, 0)) * desiredDist
|
||||
local newPos = cameraFocusP - vecToSubject
|
||||
local desiredLookDir = camera.CFrame.lookVector
|
||||
if self.RotateInput.x ~= 0 then
|
||||
desiredLookDir = vecToSubject
|
||||
end
|
||||
local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z)
|
||||
self.RotateInput = ZERO_VECTOR2
|
||||
|
||||
camera.CFrame = CFrame_new(newPos, lookAt) + Vector3_new(0, cameraHeight, 0)
|
||||
end
|
||||
else
|
||||
-- self.RotateInput is a Vector2 of mouse movement deltas since last update
|
||||
curAzimuthRad = curAzimuthRad - self.RotateInput.x
|
||||
|
||||
if useAzimuthLimits then
|
||||
curAzimuthRad = clamp(curAzimuthRad, minAzimuthAbsoluteRad, maxAzimuthAbsoluteRad)
|
||||
else
|
||||
curAzimuthRad = (curAzimuthRad ~= 0) and (math.sign(curAzimuthRad) * (math.abs(curAzimuthRad) % TAU)) or 0
|
||||
end
|
||||
|
||||
curDistance = clamp(zoom, minDistance, maxDistance)
|
||||
|
||||
curElevationRad = clamp(curElevationRad + self.RotateInput.y, minElevationRad,maxElevationRad)
|
||||
|
||||
local cameraPosVector = curDistance * ( CFrame.fromEulerAnglesYXZ( -curElevationRad, curAzimuthRad, 0 ) * UNIT_Z )
|
||||
local camPos = subjectPosition + cameraPosVector
|
||||
|
||||
camera.CFrame = CFrame.new(camPos, subjectPosition)
|
||||
|
||||
self.RotateInput = ZERO_VECTOR2
|
||||
end
|
||||
|
||||
self.LastCameraTransform = camera.CFrame
|
||||
self.LastCameraFocus = camera.Focus
|
||||
if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
|
||||
self.LastSubjectCFrame = cameraSubject.CFrame
|
||||
else
|
||||
self.LastSubjectCFrame = nil
|
||||
end
|
||||
end
|
||||
|
||||
lastUpdate = now
|
||||
end
|
||||
|
||||
function module:GetCameraZoom()
|
||||
return curDistance
|
||||
end
|
||||
|
||||
function module:ZoomCamera(desiredZoom)
|
||||
local player = PlayersService.LocalPlayer
|
||||
if player then
|
||||
curDistance = clamp(desiredZoom, minDistance, maxDistance)
|
||||
end
|
||||
|
||||
local isFirstPerson = self:GetCameraZoom() < 2
|
||||
|
||||
--ShiftLockController:SetIsInFirstPerson(isFirstPerson)
|
||||
-- set mouse behavior
|
||||
self:UpdateMouseBehavior()
|
||||
return self:GetCameraZoom()
|
||||
end
|
||||
|
||||
function module:ZoomCameraBy(zoomScale)
|
||||
local newDist = curDistance
|
||||
-- Can break into more steps to get more accurate integration
|
||||
newDist = self:rk4Integrator(curDistance, zoomScale, 1)
|
||||
self:ZoomCamera(newDist)
|
||||
return self:GetCameraZoom()
|
||||
end
|
||||
|
||||
function module:ZoomCameraFixedBy(zoomIncrement)
|
||||
return self:ZoomCamera(self:GetCameraZoom() + zoomIncrement)
|
||||
end
|
||||
|
||||
function module:SetInitialOrientation(humanoid)
|
||||
local newDesiredLook = (humanoid.Torso.CFrame.lookVector - Vector3.new(0,0.23,0)).unit
|
||||
local horizontalShift = findAngleBetweenXZVectors(newDesiredLook, self:GetCameraLook())
|
||||
local vertShift = math.asin(self:GetCameraLook().y) - math.asin(newDesiredLook.y)
|
||||
if not IsFinite(horizontalShift) then
|
||||
horizontalShift = 0
|
||||
end
|
||||
if not IsFinite(vertShift) then
|
||||
vertShift = 0
|
||||
end
|
||||
self.RotateInput = Vector2.new(horizontalShift, vertShift)
|
||||
end
|
||||
|
||||
function module.getGamepadPan(name, state, input)
|
||||
if input.UserInputType == module.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then
|
||||
if r3ButtonDown or l3ButtonDown then
|
||||
-- R3 or L3 Thumbstick is depressed, right stick controls dolly in/out
|
||||
if (input.Position.Y > THUMBSTICK_DEADZONE) then
|
||||
currentZoomSpeed = 0.96
|
||||
elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then
|
||||
currentZoomSpeed = 1.04
|
||||
else
|
||||
currentZoomSpeed = 1.00
|
||||
end
|
||||
else
|
||||
if state == Enum.UserInputState.Cancel then
|
||||
module.GamepadPanningCamera = ZERO_VECTOR2
|
||||
return
|
||||
end
|
||||
|
||||
local inputVector = Vector2.new(input.Position.X, -input.Position.Y)
|
||||
if inputVector.magnitude > THUMBSTICK_DEADZONE then
|
||||
module.GamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y)
|
||||
else
|
||||
module.GamepadPanningCamera = ZERO_VECTOR2
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function module.doGamepadZoom(name, state, input)
|
||||
if input.UserInputType == module.activeGamepad and (input.KeyCode == Enum.KeyCode.ButtonR3 or input.KeyCode == Enum.KeyCode.ButtonL3) then
|
||||
if (state == Enum.UserInputState.Begin) then
|
||||
r3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonR3
|
||||
l3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonL3
|
||||
elseif (state == Enum.UserInputState.End) then
|
||||
if (input.KeyCode == Enum.KeyCode.ButtonR3) then
|
||||
r3ButtonDown = false
|
||||
elseif (input.KeyCode == Enum.KeyCode.ButtonL3) then
|
||||
l3ButtonDown = false
|
||||
end
|
||||
if (not r3ButtonDown) and (not l3ButtonDown) then
|
||||
currentZoomSpeed = 1.00
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function module:BindGamepadInputActions()
|
||||
local ContextActionService = game:GetService('ContextActionService')
|
||||
ContextActionService:BindAction("OrbitalCamGamepadPan", module.getGamepadPan, false, Enum.KeyCode.Thumbstick2)
|
||||
ContextActionService:BindAction("OrbitalCamGamepadZoom", module.doGamepadZoom, false, Enum.KeyCode.ButtonR3)
|
||||
ContextActionService:BindAction("OrbitalCamGamepadZoomAlt", module.doGamepadZoom, false, Enum.KeyCode.ButtonL3)
|
||||
end
|
||||
|
||||
return module
|
||||
end
|
||||
|
||||
return CreateOrbitalCamera
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
local RootCameraCreator = require(script.Parent)
|
||||
|
||||
local function CreateScriptableCamera()
|
||||
local module = RootCameraCreator()
|
||||
|
||||
function module:Update()
|
||||
end
|
||||
|
||||
return module
|
||||
end
|
||||
|
||||
return CreateScriptableCamera
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
local PlayersService = game:GetService('Players')
|
||||
local RootCameraCreator = require(script.Parent)
|
||||
|
||||
local ZERO_VECTOR2 = Vector2.new(0, 0)
|
||||
|
||||
local CFrame_new = CFrame.new
|
||||
local math_min = math.min
|
||||
|
||||
local function CreateTrackCamera()
|
||||
local module = RootCameraCreator()
|
||||
|
||||
local lastUpdate = tick()
|
||||
function module:Update()
|
||||
local now = tick()
|
||||
|
||||
local userPanningTheCamera = (self.UserPanningTheCamera == true)
|
||||
local camera = workspace.CurrentCamera
|
||||
local player = PlayersService.LocalPlayer
|
||||
|
||||
if lastUpdate == nil or now - lastUpdate > 1 then
|
||||
module:ResetCameraLook()
|
||||
self.LastCameraTransform = nil
|
||||
end
|
||||
|
||||
if lastUpdate then
|
||||
-- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
|
||||
local delta = math_min(0.1, now - lastUpdate)
|
||||
local gamepadRotation = self:UpdateGamepad()
|
||||
if gamepadRotation ~= ZERO_VECTOR2 then
|
||||
userPanningTheCamera = true
|
||||
self.RotateInput = self.RotateInput + (gamepadRotation * delta)
|
||||
end
|
||||
end
|
||||
|
||||
local subjectPosition = self:GetSubjectPosition()
|
||||
if subjectPosition and player and camera then
|
||||
local zoom = self:GetCameraZoom()
|
||||
if zoom <= 0 then
|
||||
zoom = 0.1
|
||||
end
|
||||
local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput)
|
||||
self.RotateInput = ZERO_VECTOR2
|
||||
|
||||
camera.Focus = CFrame_new(subjectPosition)
|
||||
camera.CFrame = CFrame_new(subjectPosition - (zoom * newLookVector), subjectPosition)
|
||||
self.LastCameraTransform = camera.CoordinateFrame
|
||||
end
|
||||
lastUpdate = now
|
||||
end
|
||||
|
||||
return module
|
||||
end
|
||||
|
||||
return CreateTrackCamera
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
local PlayersService = game:GetService('Players')
|
||||
local UserInputService = game:GetService('UserInputService')
|
||||
local VRService = game:GetService("VRService")
|
||||
local ContextActionService = game:GetService("ContextActionService")
|
||||
|
||||
local LocalPlayer = PlayersService.LocalPlayer
|
||||
|
||||
local RootCameraCreator = require(script.Parent)
|
||||
|
||||
local XZ_VECTOR = Vector3.new(1, 0, 1)
|
||||
local ZERO_VECTOR2 = Vector2.new(0, 0)
|
||||
|
||||
local PRESNAP_TIME_OFFSET = 0.5 --seconds
|
||||
local PRESNAP_TIME_LATCH = PRESNAP_TIME_OFFSET * 10
|
||||
|
||||
local HEIGHT_OFFSET = 3
|
||||
local HORZ_OFFSET = 4
|
||||
|
||||
local function CreateVRCamera()
|
||||
local module = RootCameraCreator()
|
||||
module.AllowOcclusion = false
|
||||
|
||||
local lastUpdate = tick()
|
||||
|
||||
local forceSnap = true
|
||||
local forceSnapPoint = nil
|
||||
local lastLook = Vector3.new(0, 0, -1)
|
||||
|
||||
local movementUpdateEventConn = nil
|
||||
|
||||
local lastSnapTimeEstimate = math.huge
|
||||
local snapId = 0
|
||||
local waitingForSnap = false
|
||||
local snappedHeadOffset = VRService:GetUserCFrame(Enum.UserCFrame.Head).p
|
||||
local snapPendingWhileJumping = false
|
||||
local isJumping = false
|
||||
local autoSnapsPaused = false
|
||||
|
||||
local currentDestination = nil
|
||||
|
||||
local function forceSnapTo(snapPoint)
|
||||
forceSnap = true
|
||||
forceSnapPoint = snapPoint
|
||||
snapId = snapId + 1
|
||||
waitingForSnap = false
|
||||
end
|
||||
|
||||
local function onMovementUpdateEvent(updateType, arg1, arg2)
|
||||
if updateType == "targetPoint" then
|
||||
currentDestination = arg1
|
||||
end
|
||||
if updateType == "timing" then
|
||||
local estimatedTimeRemaining = arg1
|
||||
local snapPoint = arg2
|
||||
|
||||
if waitingForSnap and estimatedTimeRemaining > lastSnapTimeEstimate then
|
||||
--our estimate grew, so cancel this snap and potentially re-evaluate it
|
||||
waitingForSnap = false
|
||||
snapId = snapId + 1
|
||||
end
|
||||
|
||||
if estimatedTimeRemaining < PRESNAP_TIME_LATCH and estimatedTimeRemaining > PRESNAP_TIME_OFFSET then
|
||||
waitingForSnap = true
|
||||
snapId = snapId + 1
|
||||
local thisSnapId = snapId
|
||||
|
||||
local timeToWait = estimatedTimeRemaining - PRESNAP_TIME_OFFSET
|
||||
coroutine.wrap(function()
|
||||
wait(timeToWait)
|
||||
if waitingForSnap and snapId == thisSnapId then
|
||||
waitingForSnap = false
|
||||
forceSnap = true
|
||||
forceSnapPoint = snapPoint
|
||||
end
|
||||
end)()
|
||||
end
|
||||
elseif updateType == "shortPath" or updateType == "pathFailure" or updateType == "offtrack" and not autoSnapsPaused then
|
||||
if isJumping then
|
||||
snapPendingWhileJumping = true
|
||||
return
|
||||
end
|
||||
local snapPoint = arg1
|
||||
forceSnapTo(snapPoint)
|
||||
elseif updateType == "force" then
|
||||
snapPendingWhileJumping = false
|
||||
isJumping = false
|
||||
forceSnapTo(nil)
|
||||
end
|
||||
end
|
||||
|
||||
local function onResetCameraAction(actionName, inputState, inputObj)
|
||||
if inputState == Enum.UserInputState.Begin then
|
||||
autoSnapsPaused = true
|
||||
onMovementUpdateEvent("force", nil, nil)
|
||||
else
|
||||
autoSnapsPaused = false
|
||||
end
|
||||
end
|
||||
|
||||
local function bindActions(bind)
|
||||
if bind then
|
||||
ContextActionService:BindActionAtPriority("ResetCameraVR", onResetCameraAction, false, Enum.ContextActionPriority.Low.Value, Enum.KeyCode.ButtonL2)
|
||||
else
|
||||
autoSnapsPaused = false
|
||||
ContextActionService:UnbindAction("ResetCameraVR")
|
||||
end
|
||||
end
|
||||
|
||||
local lastKnownTorsoPosition = Vector3.new()
|
||||
local function onCharacterAdded(character)
|
||||
local humanoid = character:WaitForChild("Humanoid")
|
||||
|
||||
humanoid.StateChanged:connect(function(oldState, newState)
|
||||
local camera = workspace.CurrentCamera
|
||||
if not camera or camera.CameraSubject ~= humanoid or not VRService.VREnabled then
|
||||
return
|
||||
end
|
||||
if newState == Enum.HumanoidStateType.Jumping then
|
||||
isJumping = true
|
||||
elseif newState == Enum.HumanoidStateType.Landed then
|
||||
if snapPendingWhileJumping then
|
||||
forceSnapTo(nil)
|
||||
snapPendingWhileJumping = false
|
||||
end
|
||||
isJumping = false
|
||||
elseif newState == Enum.HumanoidStateType.Dead then
|
||||
forceSnapTo(nil)
|
||||
elseif newState == Enum.HumanoidStateType.Swimming then
|
||||
if isJumping and snapPendingWhileJumping then
|
||||
--Jumped into water and let go of controls during flight, treat as a normal landing
|
||||
forceSnapTo(nil)
|
||||
snapPendingWhileJumping = false
|
||||
end
|
||||
isJumping = false
|
||||
end
|
||||
end)
|
||||
|
||||
local humanoidRootPart = humanoid.Torso
|
||||
humanoidRootPart.Changed:connect(function(property)
|
||||
local camera = workspace.CurrentCamera
|
||||
local cameraSubject = camera.CameraSubject
|
||||
if camera and cameraSubject == humanoid and property == "CFrame" or property == "Position" then
|
||||
if (humanoidRootPart.Position - lastKnownTorsoPosition).magnitude > 5 then
|
||||
forceSnapTo(nil)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
if LocalPlayer.Character then
|
||||
onCharacterAdded(LocalPlayer.Character)
|
||||
end
|
||||
LocalPlayer.CharacterAdded:connect(onCharacterAdded)
|
||||
|
||||
spawn(function()
|
||||
local rootCamera = script.Parent
|
||||
if not rootCamera then return end
|
||||
|
||||
local cameraScript = rootCamera.Parent
|
||||
if not cameraScript then return end
|
||||
|
||||
local playerScripts = cameraScript.Parent
|
||||
if not playerScripts then return end
|
||||
|
||||
local controlScript = playerScripts:WaitForChild("ControlScript")
|
||||
local masterControlModule = controlScript:WaitForChild("MasterControl")
|
||||
local vrNavigationModule = masterControlModule:WaitForChild("VRNavigation")
|
||||
local movementUpdateEvent = vrNavigationModule:WaitForChild("MovementUpdate")
|
||||
|
||||
movementUpdateEventConn = movementUpdateEvent.Event:connect(onMovementUpdateEvent)
|
||||
end)
|
||||
|
||||
local onCameraSubjectChangedConn = nil
|
||||
local function onCameraSubjectChanged()
|
||||
local camera = workspace.CurrentCamera
|
||||
if camera and camera.CameraSubject then
|
||||
delay(1, function () forceSnap = true end)
|
||||
end
|
||||
end
|
||||
|
||||
local function onCurrentCameraChanged()
|
||||
if onCameraSubjectChangedConn then
|
||||
onCameraSubjectChangedConn:disconnect()
|
||||
onCameraSubjectChangedConn = nil
|
||||
end
|
||||
if workspace.CurrentCamera then
|
||||
onCameraSubjectChangedConn = workspace.CurrentCamera:GetPropertyChangedSignal("CameraSubject"):connect(onCameraSubjectChanged)
|
||||
onCameraSubjectChanged()
|
||||
end
|
||||
end
|
||||
workspace:GetPropertyChangedSignal("CurrentCamera"):connect(onCurrentCameraChanged)
|
||||
onCurrentCameraChanged()
|
||||
|
||||
local function onVREnabled()
|
||||
bindActions(VRService.VREnabled)
|
||||
end
|
||||
VRService:GetPropertyChangedSignal("VREnabled"):connect(onVREnabled)
|
||||
|
||||
function module:Update()
|
||||
local now = tick()
|
||||
local timeDelta = now - lastUpdate
|
||||
|
||||
local camera = workspace.CurrentCamera
|
||||
local cameraSubject = camera and camera.CameraSubject
|
||||
local player = PlayersService.LocalPlayer
|
||||
local subjectPosition = self:GetSubjectPosition()
|
||||
local zoom = self:GetCameraZoom()
|
||||
|
||||
local subjectPosition = self:GetSubjectPosition()
|
||||
local gamepadRotation = self:UpdateGamepad()
|
||||
|
||||
if subjectPosition and currentDestination then
|
||||
local dist = (currentDestination - subjectPosition).magnitude
|
||||
if dist < 10 then
|
||||
forceSnap = true
|
||||
forceSnapPoint = currentDestination
|
||||
currentDestination = nil
|
||||
end
|
||||
end
|
||||
|
||||
if cameraSubject and cameraSubject:IsA("Humanoid") then
|
||||
local rootPart = cameraSubject.RootPart
|
||||
if rootPart then
|
||||
lastKnownTorsoPosition = rootPart.Position
|
||||
end
|
||||
end
|
||||
|
||||
local look = lastLook
|
||||
if subjectPosition and forceSnap then
|
||||
forceSnap = false
|
||||
|
||||
local newFocusPoint = subjectPosition
|
||||
if forceSnapPoint then
|
||||
newFocusPoint = Vector3.new(forceSnapPoint.X, subjectPosition.Y, forceSnapPoint.Z)
|
||||
end
|
||||
|
||||
camera.Focus = CFrame.new(newFocusPoint)
|
||||
forceSnapPoint = nil
|
||||
look = camera:GetRenderCFrame().lookVector
|
||||
snappedHeadOffset = VRService:GetUserCFrame(Enum.UserCFrame.Head).p
|
||||
end
|
||||
if subjectPosition and player and camera then
|
||||
local cameraFocusP = camera.Focus.p
|
||||
self.RotateInput = ZERO_VECTOR2
|
||||
look = (look * XZ_VECTOR).unit
|
||||
camera.CFrame = CFrame.new(cameraFocusP - (HORZ_OFFSET * look)) + Vector3.new(0, HEIGHT_OFFSET, 0) - snappedHeadOffset
|
||||
|
||||
self.LastCameraTransform = camera.CFrame
|
||||
self.LastCameraFocus = camera.Focus
|
||||
end
|
||||
lastLook = look
|
||||
|
||||
lastUpdate = now
|
||||
end
|
||||
|
||||
return module
|
||||
end
|
||||
|
||||
return CreateVRCamera
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
local PlayersService = game:GetService('Players')
|
||||
local RootCameraCreator = require(script.Parent)
|
||||
|
||||
local ZERO_VECTOR2 = Vector2.new(0, 0)
|
||||
|
||||
local CFrame_new = CFrame.new
|
||||
|
||||
local function CreateWatchCamera()
|
||||
local module = RootCameraCreator()
|
||||
module.PanEnabled = false
|
||||
|
||||
local lastUpdate = tick()
|
||||
function module:Update()
|
||||
local now = tick()
|
||||
|
||||
local camera = workspace.CurrentCamera
|
||||
local player = PlayersService.LocalPlayer
|
||||
|
||||
if lastUpdate == nil or now - lastUpdate > 1 then
|
||||
module:ResetCameraLook()
|
||||
self.LastZoom = nil
|
||||
end
|
||||
|
||||
|
||||
local subjectPosition = self:GetSubjectPosition()
|
||||
if subjectPosition and player and camera then
|
||||
local cameraLook = nil
|
||||
|
||||
local humanoid = self:GetHumanoid()
|
||||
if humanoid and humanoid.Torso then
|
||||
-- TODO: let the paging buttons move the camera but not the mouse/touch
|
||||
-- currently neither do
|
||||
local diffVector = subjectPosition - camera.CFrame.p
|
||||
cameraLook = diffVector.unit
|
||||
|
||||
if self.LastZoom and self.LastZoom == self:GetCameraZoom() then
|
||||
-- Don't clobber the zoom if they zoomed the camera
|
||||
local zoom = diffVector.magnitude
|
||||
self:ZoomCamera(zoom)
|
||||
end
|
||||
end
|
||||
|
||||
local zoom = self:GetCameraZoom()
|
||||
if zoom <= 0 then
|
||||
zoom = 0.1
|
||||
end
|
||||
|
||||
local newLookVector = self:RotateVector(cameraLook or self:GetCameraLook(), self.RotateInput)
|
||||
self.RotateInput = ZERO_VECTOR2
|
||||
local newFocus = CFrame_new(subjectPosition)
|
||||
local newCamCFrame = CFrame_new(newFocus.p - (zoom * newLookVector), subjectPosition)
|
||||
|
||||
camera.Focus = newFocus
|
||||
camera.CFrame = newCamCFrame
|
||||
self.LastZoom = zoom
|
||||
end
|
||||
lastUpdate = now
|
||||
end
|
||||
|
||||
return module
|
||||
end
|
||||
|
||||
return CreateWatchCamera
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
--[[
|
||||
// FileName: ShiftLockController
|
||||
// Written by: jmargh
|
||||
// Version 1.2
|
||||
// Description: Manages the state of shift lock mode
|
||||
|
||||
// Required by:
|
||||
RootCamera
|
||||
|
||||
// Note: ContextActionService sinks keys, so until we allow binding to ContextActionService without sinking
|
||||
// keys, this module will use UserInputService.
|
||||
--]]
|
||||
|
||||
local Players = game:GetService('Players')
|
||||
local UserInputService = game:GetService('UserInputService')
|
||||
-- Settings and GameSettings are read only
|
||||
local Settings = UserSettings() -- ignore warning
|
||||
local GameSettings = Settings.GameSettings
|
||||
|
||||
local ShiftLockController = {}
|
||||
|
||||
--[[ Script Variables ]]--
|
||||
while not Players.LocalPlayer do
|
||||
Players.PlayerAdded:wait()
|
||||
end
|
||||
|
||||
local LocalPlayer = Players.LocalPlayer
|
||||
local Mouse = LocalPlayer:GetMouse()
|
||||
local PlayerGui = LocalPlayer:WaitForChild('PlayerGui')
|
||||
local ScreenGui = nil
|
||||
local ShiftLockIcon = nil
|
||||
local InputCn = nil
|
||||
local IsShiftLockMode = false
|
||||
local IsShiftLocked = false
|
||||
local IsActionBound = false
|
||||
local IsInFirstPerson = false
|
||||
|
||||
-- Toggle Event
|
||||
ShiftLockController.OnShiftLockToggled = Instance.new('BindableEvent')
|
||||
|
||||
-- wrapping long conditional in function
|
||||
local function isShiftLockMode()
|
||||
return LocalPlayer.DevEnableMouseLock and GameSettings.ControlMode == Enum.ControlMode.MouseLockSwitch and
|
||||
LocalPlayer.DevComputerMovementMode ~= Enum.DevComputerMovementMode.ClickToMove and
|
||||
GameSettings.ComputerMovementMode ~= Enum.ComputerMovementMode.ClickToMove and
|
||||
LocalPlayer.DevComputerMovementMode ~= Enum.DevComputerMovementMode.Scriptable
|
||||
end
|
||||
|
||||
if not UserInputService.TouchEnabled then -- TODO: Remove when safe on mobile
|
||||
IsShiftLockMode = isShiftLockMode()
|
||||
end
|
||||
|
||||
--[[ Constants ]]--
|
||||
local SHIFT_LOCK_OFF = 'rbxasset://textures/ui/mouseLock_off.png'
|
||||
local SHIFT_LOCK_ON = 'rbxasset://textures/ui/mouseLock_on.png'
|
||||
local SHIFT_LOCK_CURSOR = 'rbxasset://textures/MouseLockedCursor.png'
|
||||
|
||||
--[[ Local Functions ]]--
|
||||
local function onShiftLockToggled()
|
||||
IsShiftLocked = not IsShiftLocked
|
||||
if IsShiftLocked then
|
||||
ShiftLockIcon.Image = SHIFT_LOCK_ON
|
||||
Mouse.Icon = SHIFT_LOCK_CURSOR
|
||||
else
|
||||
ShiftLockIcon.Image = SHIFT_LOCK_OFF
|
||||
Mouse.Icon = ""
|
||||
end
|
||||
ShiftLockController.OnShiftLockToggled:Fire()
|
||||
end
|
||||
|
||||
local function initialize()
|
||||
if ScreenGui then
|
||||
ScreenGui:Destroy()
|
||||
ScreenGui = nil
|
||||
end
|
||||
ScreenGui = Instance.new('ScreenGui')
|
||||
ScreenGui.Name = "ControlGui"
|
||||
|
||||
local frame = Instance.new('Frame')
|
||||
frame.Name = "BottomLeftControl"
|
||||
frame.Size = UDim2.new(0, 130, 0, 46)
|
||||
frame.Position = UDim2.new(0, 0, 1, -46)
|
||||
frame.BackgroundTransparency = 1
|
||||
frame.Parent = ScreenGui
|
||||
|
||||
ShiftLockIcon = Instance.new('ImageButton')
|
||||
ShiftLockIcon.Name = "MouseLockLabel"
|
||||
ShiftLockIcon.Size = UDim2.new(0, 31, 0, 31)
|
||||
ShiftLockIcon.Position = UDim2.new(0, 12, 0, 2)
|
||||
ShiftLockIcon.BackgroundTransparency = 1
|
||||
ShiftLockIcon.Image = IsShiftLocked and SHIFT_LOCK_ON or SHIFT_LOCK_OFF
|
||||
ShiftLockIcon.Visible = true
|
||||
ShiftLockIcon.Parent = frame
|
||||
|
||||
ShiftLockIcon.MouseButton1Click:connect(onShiftLockToggled)
|
||||
|
||||
ScreenGui.Parent = IsShiftLockMode and PlayerGui or nil
|
||||
end
|
||||
|
||||
--[[ Public API ]]--
|
||||
function ShiftLockController:IsShiftLocked()
|
||||
return IsShiftLockMode and IsShiftLocked
|
||||
end
|
||||
|
||||
function ShiftLockController:SetIsInFirstPerson(isInFirstPerson)
|
||||
IsInFirstPerson = isInFirstPerson
|
||||
end
|
||||
|
||||
--[[ Input/Settings Changed Events ]]--
|
||||
local mouseLockSwitchFunc = function(actionName, inputState, inputObject)
|
||||
if IsShiftLockMode then
|
||||
onShiftLockToggled()
|
||||
end
|
||||
end
|
||||
|
||||
local function disableShiftLock()
|
||||
if ScreenGui then ScreenGui.Parent = nil end
|
||||
IsShiftLockMode = false
|
||||
Mouse.Icon = ""
|
||||
if InputCn then
|
||||
InputCn:disconnect()
|
||||
InputCn = nil
|
||||
end
|
||||
IsActionBound = false
|
||||
ShiftLockController.OnShiftLockToggled:Fire()
|
||||
end
|
||||
|
||||
-- TODO: Remove when we figure out ContextActionService without sinking keys
|
||||
local function onShiftInputBegan(inputObject, isProcessed)
|
||||
if isProcessed then return end
|
||||
if inputObject.UserInputType == Enum.UserInputType.Keyboard and
|
||||
(inputObject.KeyCode == Enum.KeyCode.LeftShift or inputObject.KeyCode == Enum.KeyCode.RightShift) then
|
||||
--
|
||||
mouseLockSwitchFunc()
|
||||
end
|
||||
end
|
||||
|
||||
local function enableShiftLock()
|
||||
IsShiftLockMode = isShiftLockMode()
|
||||
if IsShiftLockMode then
|
||||
if ScreenGui then
|
||||
ScreenGui.Parent = PlayerGui
|
||||
end
|
||||
if IsShiftLocked then
|
||||
Mouse.Icon = SHIFT_LOCK_CURSOR
|
||||
ShiftLockController.OnShiftLockToggled:Fire()
|
||||
end
|
||||
if not IsActionBound then
|
||||
InputCn = UserInputService.InputBegan:connect(onShiftInputBegan)
|
||||
IsActionBound = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
GameSettings.Changed:connect(function(property)
|
||||
if property == 'ControlMode' then
|
||||
if GameSettings.ControlMode == Enum.ControlMode.MouseLockSwitch then
|
||||
enableShiftLock()
|
||||
else
|
||||
disableShiftLock()
|
||||
end
|
||||
elseif property == 'ComputerMovementMode' then
|
||||
if GameSettings.ComputerMovementMode == Enum.ComputerMovementMode.ClickToMove then
|
||||
disableShiftLock()
|
||||
else
|
||||
enableShiftLock()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
LocalPlayer.Changed:connect(function(property)
|
||||
if property == 'DevEnableMouseLock' then
|
||||
if LocalPlayer.DevEnableMouseLock then
|
||||
enableShiftLock()
|
||||
else
|
||||
disableShiftLock()
|
||||
end
|
||||
elseif property == 'DevComputerMovementMode' then
|
||||
if LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.ClickToMove or
|
||||
LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.Scriptable then
|
||||
--
|
||||
disableShiftLock()
|
||||
else
|
||||
enableShiftLock()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
LocalPlayer.CharacterAdded:connect(function(character)
|
||||
-- we need to recreate guis on character load
|
||||
if not UserInputService.TouchEnabled then
|
||||
initialize()
|
||||
end
|
||||
end)
|
||||
|
||||
--[[ Initialization ]]--
|
||||
-- TODO: Remove when safe! ContextActionService crashes touch clients with tupele is 2 or more
|
||||
if not UserInputService.TouchEnabled then
|
||||
initialize()
|
||||
if isShiftLockMode() then
|
||||
InputCn = UserInputService.InputBegan:connect(onShiftInputBegan)
|
||||
IsActionBound = true
|
||||
end
|
||||
end
|
||||
|
||||
return ShiftLockController
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
-- SolarCrane
|
||||
|
||||
local MAX_TWEEN_RATE = 2.8 -- per second
|
||||
|
||||
local function clamp(low, high, num)
|
||||
return (num > high and high or num < low and low or num)
|
||||
end
|
||||
|
||||
local math_floor = math.floor
|
||||
|
||||
local function Round(num, places)
|
||||
local decimalPivot = 10^places
|
||||
return math_floor(num * decimalPivot + 0.5) / decimalPivot
|
||||
end
|
||||
|
||||
local function CreateTransparencyController()
|
||||
local module = {}
|
||||
|
||||
|
||||
local LastUpdate = tick()
|
||||
local TransparencyDirty = false
|
||||
local Enabled = false
|
||||
local LastTransparency = nil
|
||||
|
||||
local DescendantAddedConn, DescendantRemovingConn = nil, nil
|
||||
local ToolDescendantAddedConns = {}
|
||||
local ToolDescendantRemovingConns = {}
|
||||
local CachedParts = {}
|
||||
|
||||
local function HasToolAncestor(object)
|
||||
if object.Parent == nil then return false end
|
||||
return object.Parent:IsA('Tool') or HasToolAncestor(object.Parent)
|
||||
end
|
||||
|
||||
local function IsValidPartToModify(part)
|
||||
if part:IsA('BasePart') or part:IsA('Decal') then
|
||||
return not HasToolAncestor(part)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function CachePartsRecursive(object)
|
||||
if object then
|
||||
if IsValidPartToModify(object) then
|
||||
CachedParts[object] = true
|
||||
TransparencyDirty = true
|
||||
end
|
||||
for _, child in pairs(object:GetChildren()) do
|
||||
CachePartsRecursive(child)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function TeardownTransparency()
|
||||
for child, _ in pairs(CachedParts) do
|
||||
child.LocalTransparencyModifier = 0
|
||||
end
|
||||
CachedParts = {}
|
||||
TransparencyDirty = true
|
||||
LastTransparency = nil
|
||||
|
||||
if DescendantAddedConn then
|
||||
DescendantAddedConn:disconnect()
|
||||
DescendantAddedConn = nil
|
||||
end
|
||||
if DescendantRemovingConn then
|
||||
DescendantRemovingConn:disconnect()
|
||||
DescendantRemovingConn = nil
|
||||
end
|
||||
for object, conn in pairs(ToolDescendantAddedConns) do
|
||||
conn:disconnect()
|
||||
ToolDescendantAddedConns[object] = nil
|
||||
end
|
||||
for object, conn in pairs(ToolDescendantRemovingConns) do
|
||||
conn:disconnect()
|
||||
ToolDescendantRemovingConns[object] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function SetupTransparency(character)
|
||||
TeardownTransparency()
|
||||
|
||||
if DescendantAddedConn then DescendantAddedConn:disconnect() end
|
||||
DescendantAddedConn = character.DescendantAdded:connect(function(object)
|
||||
-- This is a part we want to invisify
|
||||
if IsValidPartToModify(object) then
|
||||
CachedParts[object] = true
|
||||
TransparencyDirty = true
|
||||
-- There is now a tool under the character
|
||||
elseif object:IsA('Tool') then
|
||||
if ToolDescendantAddedConns[object] then ToolDescendantAddedConns[object]:disconnect() end
|
||||
ToolDescendantAddedConns[object] = object.DescendantAdded:connect(function(toolChild)
|
||||
CachedParts[toolChild] = nil
|
||||
if toolChild:IsA('BasePart') or toolChild:IsA('Decal') then
|
||||
-- Reset the transparency
|
||||
toolChild.LocalTransparencyModifier = 0
|
||||
end
|
||||
end)
|
||||
if ToolDescendantRemovingConns[object] then ToolDescendantRemovingConns[object]:disconnect() end
|
||||
ToolDescendantRemovingConns[object] = object.DescendantRemoving:connect(function(formerToolChild)
|
||||
wait() -- wait for new parent
|
||||
if character and formerToolChild and formerToolChild:IsDescendantOf(character) then
|
||||
if IsValidPartToModify(formerToolChild) then
|
||||
CachedParts[formerToolChild] = true
|
||||
TransparencyDirty = true
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
if DescendantRemovingConn then DescendantRemovingConn:disconnect() end
|
||||
DescendantRemovingConn = character.DescendantRemoving:connect(function(object)
|
||||
if CachedParts[object] then
|
||||
CachedParts[object] = nil
|
||||
-- Reset the transparency
|
||||
object.LocalTransparencyModifier = 0
|
||||
end
|
||||
end)
|
||||
CachePartsRecursive(character)
|
||||
end
|
||||
|
||||
|
||||
function module:SetEnabled(newState)
|
||||
if Enabled ~= newState then
|
||||
Enabled = newState
|
||||
self:Update()
|
||||
end
|
||||
end
|
||||
|
||||
function module:SetSubject(subject)
|
||||
local character = nil
|
||||
if subject and subject:IsA("Humanoid") then
|
||||
character = subject.Parent
|
||||
end
|
||||
if subject and subject:IsA("VehicleSeat") and subject.Occupant then
|
||||
character = subject.Occupant.Parent
|
||||
end
|
||||
if character then
|
||||
SetupTransparency(character)
|
||||
else
|
||||
TeardownTransparency()
|
||||
end
|
||||
end
|
||||
|
||||
function module:Update()
|
||||
local instant = false
|
||||
local now = tick()
|
||||
local currentCamera = workspace.CurrentCamera
|
||||
|
||||
if currentCamera then
|
||||
local transparency = 0
|
||||
if not Enabled then
|
||||
instant = true
|
||||
else
|
||||
local distance = (currentCamera.Focus.p - currentCamera.CoordinateFrame.p).magnitude
|
||||
transparency = (7 - distance) / 5
|
||||
if transparency < 0.5 then
|
||||
transparency = 0
|
||||
end
|
||||
|
||||
if LastTransparency then
|
||||
local deltaTransparency = transparency - LastTransparency
|
||||
-- Don't tween transparency if it is instant or your character was fully invisible last frame
|
||||
if not instant and transparency < 1 and LastTransparency < 0.95 then
|
||||
local maxDelta = MAX_TWEEN_RATE * (now - LastUpdate)
|
||||
deltaTransparency = clamp(-maxDelta, maxDelta, deltaTransparency)
|
||||
end
|
||||
transparency = LastTransparency + deltaTransparency
|
||||
else
|
||||
TransparencyDirty = true
|
||||
end
|
||||
|
||||
transparency = clamp(0, 1, Round(transparency, 2))
|
||||
end
|
||||
|
||||
if TransparencyDirty or LastTransparency ~= transparency then
|
||||
for child, _ in pairs(CachedParts) do
|
||||
child.LocalTransparencyModifier = transparency
|
||||
end
|
||||
TransparencyDirty = false
|
||||
LastTransparency = transparency
|
||||
end
|
||||
end
|
||||
LastUpdate = now
|
||||
end
|
||||
|
||||
return module
|
||||
end
|
||||
|
||||
return CreateTransparencyController
|
||||
Reference in New Issue
Block a user