--[[ Invisicam - Occlusion module that makes objects occluding character view semi-transparent 2018 Camera Update - AllYourBlox --]] --[[ Camera Maths Utilities Library ]]-- local Util = require(script.Parent:WaitForChild("CameraUtils")) --[[ Top Level Roblox Services ]]-- local PlayersService = game:GetService("Players") local RunService = game:GetService("RunService") --[[ Constants ]]-- local ZERO_VECTOR3 = Vector3.new(0,0,0) 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, -- Retired, unutilized 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, -- Dynamic outline around the character } local LIMB_TRACKING_SET = { -- Body parts common to R15 and R6 ['Head'] = true, -- Body parts unique to R6 ['Left Arm'] = true, ['Right Arm'] = true, ['Left Leg'] = true, ['Right Leg'] = true, -- Body parts unique to R15 ['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 -- Used to sanitize user-supplied functions 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 -- Helper function for Determinant of 3x3, not in CameraUtils for performance reasons 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 --[[ The Module ]]-- local BaseOcclusion = require(script.Parent:WaitForChild("BaseOcclusion")) local Invisicam = setmetatable({}, BaseOcclusion) Invisicam.__index = Invisicam function Invisicam.new() local self = setmetatable(BaseOcclusion.new(), Invisicam) self.character = nil self.humanoidRootPart = nil self.torsoPart = nil self.headPart = nil self.childAddedConn = nil self.childRemovedConn = nil self.behaviors = {} -- Map of modes to behavior fns self.behaviors[MODE.LIMBS] = self.LimbBehavior self.behaviors[MODE.MOVEMENT] = self.MoveBehavior self.behaviors[MODE.CORNERS] = self.CornerBehavior self.behaviors[MODE.CIRCLE1] = self.CircleBehavior self.behaviors[MODE.CIRCLE2] = self.CircleBehavior self.behaviors[MODE.LIMBMOVE] = self.LimbMoveBehavior self.behaviors[MODE.SMART_CIRCLE] = self.SmartCircleBehavior self.behaviors[MODE.CHAR_OUTLINE] = self.CharacterOutlineBehavior self.mode = MODE.SMART_CIRCLE self.behaviorFunction = self.SmartCircleBehavior self.savedHits = {} -- Objects currently being faded in/out self.trackedLimbs = {} -- Used in limb-tracking casting modes self.camera = game.Workspace.CurrentCamera self.enabled = false return self end function Invisicam:Enable(enable) self.enabled = enable if not enable then self:Cleanup() end end function Invisicam:GetOcclusionMode() return Enum.DevCameraOcclusionMode.Invisicam end --[[ Module functions ]]-- function Invisicam:LimbBehavior(castPoints) for limb, _ in pairs(self.trackedLimbs) do castPoints[#castPoints + 1] = limb.Position end end function Invisicam:MoveBehavior(castPoints) for i = 1, MOVE_CASTS do local position, velocity = self.humanoidRootPart.Position, self.humanoidRootPart.Velocity local horizontalSpeed = Vector3.new(velocity.X, 0, velocity.Z).Magnitude / 2 local offsetVector = (i - 1) * self.humanoidRootPart.CFrame.lookVector * horizontalSpeed castPoints[#castPoints + 1] = position + offsetVector end end function Invisicam:CornerBehavior(castPoints) local cframe = self.humanoidRootPart.CFrame local centerPoint = cframe.p local rotation = cframe - centerPoint local halfSize = self.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 function Invisicam:CircleBehavior(castPoints) local cframe = nil if self.mode == MODE.CIRCLE1 then cframe = self.humanoidRootPart.CFrame else local camCFrame = self.camera.CoordinateFrame cframe = camCFrame - camCFrame.p + self.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 function Invisicam:LimbMoveBehavior(castPoints) self:LimbBehavior(castPoints) self:MoveBehavior(castPoints) end function Invisicam:CharacterOutlineBehavior(castPoints) local torsoUp = self.torsoPart.CFrame.upVector.unit local torsoRight = self.torsoPart.CFrame.rightVector.unit -- Torso cross of points for interior coverage castPoints[#castPoints + 1] = self.torsoPart.CFrame.p castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoUp castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoUp castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoRight castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoRight if self.headPart then castPoints[#castPoints + 1] = self.headPart.CFrame.p end local cframe = CFrame.new(ZERO_VECTOR3,Vector3.new(self.camera.CoordinateFrame.lookVector.X,0,self.camera.CoordinateFrame.lookVector.Z)) local centerPoint = (self.torsoPart and self.torsoPart.Position or self.humanoidRootPart.Position) local partsWhitelist = {self.torsoPart} if self.headPart then partsWhitelist[#partsWhitelist + 1] = self.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 function Invisicam:SmartCircleBehavior(castPoints) local torsoUp = self.torsoPart.CFrame.upVector.unit local torsoRight = self.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] = self.torsoPart.CFrame.p castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoUp castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoUp castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoRight castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoRight if self.headPart then castPoints[#castPoints + 1] = self.headPart.CFrame.p end local cameraOrientation = self.camera.CFrame - self.camera.CFrame.p local torsoPoint = Vector3.new(0,0.5,0) + (self.torsoPart and self.torsoPart.Position or self.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 - self.camera.CFrame.p local ray = Ray.new(torsoPoint, circlePoint - torsoPoint) local hit, hp, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.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 - self.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, {self.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, {self.character}, false, false ) if hit then local castPoint2 = hitPoint - 0.1 * (castPoint - torsoPoint).unit castPoint = castPoint2 end end castPoints[#castPoints + 1] = castPoint end end function Invisicam:CheckTorsoReference() if self.character then self.torsoPart = self.character:FindFirstChild("Torso") if not self.torsoPart then self.torsoPart = self.character:FindFirstChild("UpperTorso") if not self.torsoPart then self.torsoPart = self.character:FindFirstChild("HumanoidRootPart") end end self.headPart = self.character:FindFirstChild("Head") end end function Invisicam:CharacterAdded(player, character) -- We only want the LocalPlayer's character if player~=PlayersService.LocalPlayer then return end if self.childAddedConn then self.childAddedConn:Disconnect() self.childAddedConn = nil end if self.childRemovedConn then self.childRemovedConn:Disconnect() self.childRemovedConn = nil end self.character = character self.trackedLimbs = {} local function childAdded(child) if child:IsA("BasePart") then if LIMB_TRACKING_SET[child.Name] then self.trackedLimbs[child] = true end if (child.Name == "Torso" or child.Name == "UpperTorso") then self.torsoPart = child end if (child.Name == "Head") then self.headPart = child end end end local function childRemoved(child) self.trackedLimbs[child] = nil -- If removed/replaced part is 'Torso' or 'UpperTorso' double check that we still have a TorsoPart to use self:CheckTorsoReference() end self.childAddedConn = character.ChildAdded:Connect(childAdded) self.childRemovedConn = character.ChildRemoved:Connect(childRemoved) for _, child in pairs(self.character:GetChildren()) do childAdded(child) end end function Invisicam:SetMode(newMode) AssertTypes(newMode, 'number') for modeName, modeNum in pairs(MODE) do if modeNum == newMode then self.mode = newMode self.behaviorFunction = self.behaviors[self.mode] return end end error("Invalid mode number") end function Invisicam:GetObscuredParts() return self.savedHits end -- Want to turn off Invisicam? Be sure to call this after. function Invisicam:Cleanup() for hit, originalFade in pairs(self.savedHits) do hit.LocalTransparencyModifier = originalFade end end function Invisicam:Update(dt, desiredCameraCFrame, desiredCameraFocus) -- Bail if there is no Character if not self.enabled or not self.character then return desiredCameraCFrame, desiredCameraFocus end self.camera = game.Workspace.CurrentCamera -- TODO: Move this to a GetHumanoidRootPart helper, probably combine with CheckTorsoReference -- Make sure we still have a HumanoidRootPart if not self.humanoidRootPart then local humanoid = self.character:FindFirstChildOfClass("Humanoid") if humanoid and humanoid.RootPart then self.humanoidRootPart = humanoid.RootPart else -- Not set up with Humanoid? Try and see if there's one in the Character at all: self.humanoidRootPart = self.character:FindFirstChild("HumanoidRootPart") if not self.humanoidRootPart then -- Bail out, since we're relying on HumanoidRootPart existing return desiredCameraCFrame, desiredCameraFocus end end -- TODO: Replace this with something more sensible local ancestryChangedConn ancestryChangedConn = self.humanoidRootPart.AncestryChanged:Connect(function(child, parent) if child == self.humanoidRootPart and not parent then self.humanoidRootPart = nil if ancestryChangedConn and ancestryChangedConn.Connected then ancestryChangedConn:Disconnect() ancestryChangedConn = nil end end end) end if not self.torsoPart then self:CheckTorsoReference() if not self.torsoPart then -- Bail out, since we're relying on Torso existing, should never happen since we fall back to using HumanoidRootPart as torso return desiredCameraCFrame, desiredCameraFocus end end -- Make a list of world points to raycast to local castPoints = {} self.behaviorFunction(self, castPoints) -- Cast to get a list of objects between the camera and the cast points local currentHits = {} local ignoreList = {self.character} local function add(hit) currentHits[hit] = true if not self.savedHits[hit] then self.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 = self.headPart and self.headPart.CFrame.p or castPoints[1] local torsoPoint = self.torsoPart and self.torsoPart.CFrame.p or castPoints[2] hitParts = self.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 = self.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(self.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 self.savedHits[hitPart] = nil end end -- Invisicam does not change the camera values return desiredCameraCFrame, desiredCameraFocus end return Invisicam