Welp. I’ve tried this on my own for some time now, but I can’t figure it out.
Essentially, in my game, the viewmodel is completely diegetic, so I use the character model for pretty much everything. It kind of sucks, but I think it looks cool. An important thing to note is that I create a virtual local character, which mirrors the character but has the CFrames of each limb moves slightly backwards based on camera rotation. I made this system a while ago, so I don’t remember the specifics, but it works.
Anyway, my issue right now is that when I aim down sights, the animation is fine, but the sights are not quite aligned, and having to create an animation with perfectly aligned sights would be impossible, especially with the virtual character, since it’s slightly misaligned. Therefore, I need to slightly adjust the arms based on the difference between a given attachment’s worldcframe which marks the sight and the camera cframe.
Here is a visual representation of what I’m trying to do, since I know I suck at explaining things verbally:
And here’s my current implementation:
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local TweenService = game:GetService("TweenService")
local ReplStore = game:GetService("ReplicatedStorage")
local Camera = workspace.CurrentCamera
local Timer = require(ReplStore.Modules.Timer)
local Player = game:GetService("Players").LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local Humanoid = Character:WaitForChild("Humanoid")
local RootPart = Character:WaitForChild("HumanoidRootPart")
local RootJoint = Character:WaitForChild("HumanoidRootPart"):WaitForChild("RootJoint")
local Torso = Character:WaitForChild("Torso")
local VChar = Camera:WaitForChild("VChar")
local Tool = script.Parent
local CurrentWeaponModel = nil
local VirtualWeaponModel = nil
local WeaponAnimations = {}
local AnimationsExcludedFromDynamicOffset = {
'OffHipIdle'
}
local function CacheWeaponAnims(WeaponName)
WeaponAnimations = {}
for _, Animation in ReplStore.Animations:FindFirstChild(WeaponName):GetChildren() do
WeaponAnimations[Animation.Name] = Humanoid.Animator:LoadAnimation(Animation)
end
end
local function LoadWeaponModel(WeaponModelName, _Character)
local BaseWeaponModel = ReplStore.Items:FindFirstChild(WeaponModelName)
assert(BaseWeaponModel ~= nil, `Weapon model {WeaponModelName} doesn't exist!`)
local WeaponModel = BaseWeaponModel:Clone()
WeaponModel.Parent = _Character
_Character:FindFirstChild('Right Arm'):FindFirstChild('Handle').Part0 = _Character:FindFirstChild('Right Arm')
_Character:FindFirstChild('Right Arm'):FindFirstChild('Handle').Part1 = WeaponModel:FindFirstChild('Handle')
return WeaponModel
end
Tool.Equipped:Connect(function()
CurrentWeaponModel = LoadWeaponModel(Tool.Name, Character)
VirtualWeaponModel = LoadWeaponModel(Tool.Name, VChar)
CacheWeaponAnims(Tool.Name)
WeaponAnimations.OffHipIdle:Play()
end)
Tool.Unequipped:Connect(function()
for _, Animation in WeaponAnimations do
Animation:Stop()
end
-- todo, once equip/unequip animations arrive: play unequip animation
CurrentWeaponModel:Destroy()
VirtualWeaponModel:Destroy()
CurrentWeaponModel = nil
VirtualWeaponModel = nil
end)
local LeftShoulder = Character.Torso['Left Shoulder']
local RightShoulder = Character.Torso['Right Shoulder']
local LeftShoulderC0 = LeftShoulder.C0
local RightShoulderC0 = RightShoulder.C0
local LeftShoulderC1 = LeftShoulder.C1
local RightShoulderC1 = RightShoulder.C1
local Sine = 0
local function map(x, in_min, in_max, out_min, out_max)
return out_min + (x - in_min)*(out_max - out_min)/(in_max - in_min)
end
local function lerp(a, b, x)
return a + (b - a) * x
end
local function CalculateXShift_Quadratic(x)
return (0.5 * x ^ 2) + (1.0 * x)
end
local function WorldSpaceToJointC0Space(Joint, WorldCFrame)
local Part1CF = Joint.Part1.CFrame
local Part0 = Joint.Part0
local C0Copy = Joint.C0
local C1Copy = Joint.C1
return Part0.CFrame:Inverse() * WorldCFrame * C1Copy
end
local DynamicOffsetBlend = 0.0
local AimOffsetBlend = 0.0
local AimOffsetTarget = 0.0
RunService.PreAnimation:Connect(function(dt)
if not CurrentWeaponModel then return end
VChar['Right Arm'].Handle.Transform = Character['Right Arm'].Handle.Transform
local ShouldCalculateOffset = true
for AnimationName, Animation in WeaponAnimations do
if not Animation.IsPlaying then continue end
if table.find(AnimationsExcludedFromDynamicOffset, AnimationName) then
ShouldCalculateOffset = false
end
end
if ShouldCalculateOffset then
DynamicOffsetBlend = lerp(DynamicOffsetBlend, 1, 0.06 * dt * 60)
else
DynamicOffsetBlend = lerp(DynamicOffsetBlend, 0, 0.06 * dt * 60)
end
AimOffsetBlend = lerp(AimOffsetTarget, AimOffsetTarget, 0.1 * dt * 60)
if DynamicOffsetBlend > 0.01 then
local LocalCamera = RootPart.CFrame:ToObjectSpace(Camera.CFrame)
local Offset = CFrame.lookAlong(Vector3.new(0, 0, 0), LocalCamera.LookVector)
local OffsetAngle = Vector3.new(Offset:ToEulerAnglesYXZ())
local XShift = CalculateXShift_Quadratic(OffsetAngle.X)
local YShift = map(OffsetAngle.X, -1.4, 1.4, -1.0, 1.0)
local LeftOffset = CFrame.new(Offset.Position + Vector3.new(YShift, XShift, 0)) * CFrame.Angles(0, 0, OffsetAngle.X)
local RightOffset = CFrame.new(Offset.Position + Vector3.new(-YShift, XShift, 0)) * CFrame.Angles(0, 0, -OffsetAngle.X)
--LeftShoulder.Transform = LeftShoulder.Transform:Lerp(LeftShoulder.Transform:ToObjectSpace(LeftOffset):Inverse(), DynamicOffsetBlend)
--RightShoulder.Transform = RightShoulder.Transform:Lerp(RightShoulder.Transform:ToObjectSpace(RightOffset):Inverse(), DynamicOffsetBlend)
end
RightShoulder.C0 = RightShoulderC0
LeftShoulder.C0 = LeftShoulderC0
RightShoulder.C1 = RightShoulderC1
LeftShoulder.C1 = LeftShoulderC1
-- should be using the blend, i know - this is for experimenting
if AimOffsetTarget > 0.01 then
local RearSightAttachment = CurrentWeaponModel.Handle['RearSightRef']
local FrontSightAttachment = CurrentWeaponModel.Handle['FrontSightRef']
local CurrentWorldAim = CFrame.lookAt(RearSightAttachment.WorldPosition, FrontSightAttachment.WorldPosition)
local TargetWorldAim = Camera.CFrame + (Camera.CFrame.LookVector * 3)
local AimTransformation = CurrentWorldAim:Inverse() * TargetWorldAim
local RightWorldTransformation = WorldSpaceToJointC0Space(RightShoulder, CurrentWorldAim * AimTransformation)
local LeftWorldTransformation = WorldSpaceToJointC0Space(LeftShoulder, CurrentWorldAim * AimTransformation)
RightShoulder.C0 = RightWorldTransformation
LeftShoulder.C0 = LeftWorldTransformation
end
end)
local HipTimer = Timer.new(5)
UserInputService.InputBegan:Connect(function(Input, Processed)
if Processed then return end
if Tool.Parent ~= Character then return end
if Input.UserInputType == Enum.UserInputType.MouseButton2 then
WeaponAnimations.OffHipIdle:Stop(0.25)
WeaponAnimations.HipIdle:Stop(0.25)
WeaponAnimations.AimIdle:Play(0.25)
AimOffsetTarget = 1.0
end
end)
UserInputService.InputEnded:Connect(function(Input, Processed)
if Processed then return end
if Tool.Parent ~= Character then return end
if Input.UserInputType == Enum.UserInputType.MouseButton2 then
WeaponAnimations.AimIdle:Stop(0.25)
WeaponAnimations.HipIdle:Play(0.25)
AimOffsetTarget = 0.0
HipTimer:Set(5)
HipTimer:Start()
end
end)
HipTimer.Ended:Connect(function()
print('switching to OffHipIdle')
WeaponAnimations.HipIdle:Stop(0.5)
WeaponAnimations.OffHipIdle:Play(0.5)
end)