Custom Camera bob animation for ViewModel

I currently have a viewmodel with a custom camera animation for head bobbing and arms. The only problem is that when I set the camera cframe to the part’s frame it doesn’t apply the ability to look around as it’s constantly setting the cframe to the animated head bob part cframe. I was thinking of doing nested cameras, enabling the second camera while the first camera allows the ability to look around all while maintaining the ability to see the camera shakes, but unfortunately Roblox studio doesn’t allow this (might be wrong). Is there another way to do this? Or Do I have to take the long approach and use math? Let me know if you need to see the animation or the hierarchy of the viewmodel

3 Likes

Do you have a code sample? I don’t think any of your proposed solution is necessary

Id have to get home to get the code. If you encountered this, how would you approach this?

I would use the CameraOffset property of humanoids to bob the camera up and down. CameraOffset shifts the point your camera orbits your character, relative to the character’s CFrame.


This is my approach after messing around a bit with a procedural camera bob using math.sin()
It’s not very refined, but the general idea is there
viewmodel thing.rbxl (82.6 KB)

LocalScript
--[[ Variables ]]--
-- Services --
local CollectionService = game:GetService("CollectionService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

-- Character --
local character = script.Parent
local rootPart = character:WaitForChild("HumanoidRootPart")
local humanoid = character:WaitForChild("Humanoid")

local characterJoints = {
	leftShoulder = character:WaitForChild("Torso"):WaitForChild("Left Shoulder"),
	rightShoulder = character:WaitForChild("Torso"):WaitForChild("Right Shoulder"),
}

-- Viewmodel --
local camera = workspace.CurrentCamera
local viewmodel = ReplicatedStorage.ViewmodelRig:Clone()
local viewmodelJoints = {
	leftShoulder = viewmodel.Torso["Left Shoulder"],
	rightShoulder = viewmodel.Torso["Right Shoulder"],
}

local currentBobTick = 0
local currentBobTransform = Vector3.zero

local currentTilt = CFrame.new()

-- Constants --
local CAMERA_TILT_ANGLE = math.rad(5)

local VIEWMODEL_CAMERA_OFFSET = CFrame.new(0, 0, -1)

local BOB_UP_AMPLITUDE = 0.5
local BOB_UP_FREQUENCY = 12
local BOB_RIGHT_AMPLITUDE = 0.5
local BOB_RIGHT_FREQUENCY = 6

--[[ Functions ]]--
local function animateViewmodelJoints() : ()
	for jointName, joint in viewmodelJoints do
		joint.Transform = characterJoints[jointName].Transform
		joint.Part1.LocalTransparencyModifier = 0
	end
end

local function getBobTransform(dt : number) : Vector3
	local isBobbing = humanoid.MoveDirection.Magnitude > 0
	if not isBobbing then
		currentBobTick = 0
		return currentBobTransform:Lerp(Vector3.zero, dt * 3)
	end
	
	currentBobTick += dt
	
	local bobUp = math.sin(math.pi / 2 * currentBobTick * BOB_UP_FREQUENCY) * BOB_UP_AMPLITUDE
	local bobRight = math.sin(math.pi / 2 * currentBobTick * BOB_RIGHT_FREQUENCY) * BOB_RIGHT_AMPLITUDE
	local bob = Vector3.new(bobRight, bobUp, 0)
	
	--// The way CameraOffset works is by shifting the point your camera orbits your character
	--// This shift is relative to the character's CFrame
	--// In first person this isn't an issue since your character and camera are always aligned, but
	--// in third person, if your character isn't looking where your camera is, the bobbing will
	--// go "into" the character
	--// The following code simply ensures the camera offset is always relative to the camera
	local look = (camera.CFrame.LookVector * Vector3.new(1, 0, 1)).Unit
	local flatCameraCFrame = CFrame.lookAlong(Vector3.zero, look)
	
	local v0 = flatCameraCFrame:VectorToObjectSpace(bob)
	local v1 = rootPart.CFrame:VectorToObjectSpace(v0)
	
	return currentBobTransform:Lerp(v1, 0.2)
end

local function getCameraTilt(dt : number) : CFrame
	local dot = camera.CFrame.RightVector:Dot(humanoid.MoveDirection)

	local angle = CAMERA_TILT_ANGLE * -dot
	local tilt = CFrame.Angles(0, 0, angle)
	
	return currentTilt:Lerp(tilt, dt * 10)
end

local function updateCamera(dt : number) : ()
	local bobTransform = getBobTransform(dt)
	currentBobTransform = bobTransform
	humanoid.CameraOffset = bobTransform
	
	local cameraCFrame = camera.CFrame
	local cameraTilt = getCameraTilt(dt)
	cameraCFrame *= cameraTilt
	camera:PivotTo(cameraCFrame)
	currentTilt = cameraTilt
	
	local viewmodelCFrame = camera.CFrame * VIEWMODEL_CAMERA_OFFSET * CFrame.new(-bobTransform / 4)
	viewmodel:PivotTo(viewmodelCFrame)
	
	animateViewmodelJoints()
end

local function clearViewmodels() : ()
	for _, v in CollectionService:GetTagged("Viewmodel") do
		v:Destroy()
	end
end

local function initViewmodel() : ()
	viewmodel.Parent = camera
	viewmodel.Torso.CanCollide = false

	characterJoints.leftShoulder.Enabled = false
	characterJoints.rightShoulder.Enabled = false

	viewmodelJoints.leftShoulder.Enabled = true
	viewmodelJoints.leftShoulder.Part1 = characterJoints.leftShoulder.Part1
	viewmodelJoints.rightShoulder.Enabled = true
	viewmodelJoints.rightShoulder.Part1 = characterJoints.rightShoulder.Part1
end

local function init() : ()
	clearViewmodels()
	initViewmodel()
	
	RunService:BindToRenderStep("viewmodel_update", Enum.RenderPriority.Camera.Value + 1, updateCamera)
end
init()
3 Likes

In my case above, where I already have a specific animation for the camera bob rather than using math. Math right now is my last resort

I don’t think there’s any other efficient way of making camera bob animations other than with math (math.cos, math.sin & math.abs).

A popular solution would be something like this:

local RunService = game:GetService(“RunService”)
local Players = game:GetService(“Players”)

local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild(“Humanoid”)

RunService.RenderStepped:Connect(function()
if humanoid.MoveDirection.Magnitude > 0 then
local x = math.sin(tick() * 5) * 0.5
local y = math.abs(math.cos * 5) * 0.5

           humanoid.CameraOffset = humanoid.CameraOffset:Lerp(Vector3.new(x, y, 0), 0.25)
    else
           humanoid.CameraOffset *= 0.85
    end

end)

Sorry for any typos or if I messed up something, I’m on mobile so I don’t have any way of checking if the code works but it should be able to provide you with a general idea of how to implement a camera bob.

Hey maybe we should request a new feature for nested cameras (unity / unreal engine like cameras) Are you guys down?

1 Like

If you’re dead-set on using the pre-computed animation, the way I’d do it is to load its KeyframeSequence using AnimationClipProvider:GetAnimationClipAsync() and then “step” the animation by each frame delta and then set the humanoid CameraOffset to the head’s transform at the stepped timestamp

1 Like

Will definitely try this. Ill let you know if it works

Yeah I wasn’t able to do it. Might just resort to math. The only disadvantage to this is that it’s gonna take some time tweaking the configuration settings to sync the animation with the camera bobbing if you want it to be realistic.

If you want to mess around with it I can send over the viewmodel, but for now I might have to use math so I can keep progressing with my game. DM me if you found a workaround
viewmodel.rbxm (25.6 KB)

If you have a clip of what you want the bob to resemble I could try and write a procedural animation for it

I’ve managed to somewhat replicate this using math. If you couldn’t tell, there are micro tremors present in the rotations of the animation
The video can be seen here

It is hard to make out the tremors, so this is the best I was able to replicate. I’ve attached a demo file if you’d like to experiment with it. Rotating on more than just one axis is possible, and would be pretty simple to implement with a similar algorithm I used here


camera thing 2.rbxl (66.0 KB)

local function animate(t : number) : CFrame
	local bob = math.sin(t * math.pi / 2 * bobFrequency) * bobHeight
	local animateCFrame = CFrame.new(0, bob, 0)
	
	local shake = math.sin(t * math.pi / 2 * shakeFrequency) * shakeDegree
	animateCFrame *= CFrame.Angles(0, math.rad(shake), 0)
	
	return animateCFrame
end

I have a similar implementation, just a bit more complicated and I seem to like it. What do you think?

Wow that sin curve on the black block is super cool, how did you do that?

That looks pretty good!

It’s a debugging function I wrote to visualize the camera effects, it just casts a ray after updating the camera offset and puts a dot on anything it hits

local function makeDot()
	local dotPosition = Vector3.zero
	
	local castOrigin = workspace.CurrentCamera.CFrame.Position
	local castDirection = workspace.CurrentCamera.CFrame.LookVector * 15
	local castResult = workspace:Raycast(castOrigin, castDirection)
	if castResult then
		dotPosition = castResult.Position
	else
		dotPosition = castOrigin + castDirection
	end
	
	local dot = Instance.new("Part")
	dot.Size = Vector3.new(0.05, 0.05, 0.05)
	dot.Position = dotPosition
	dot.BrickColor = BrickColor.Red()
	dot.Anchored = true
	dot.CanCollide = false
	dot.CanQuery = false
	dot.Shape = Enum.PartType.Block
	dot.Parent = workspace
	Debris:AddItem(dot, 0.5)
end
1 Like

lol if it hits anything but the part it’s gonna be a bunch of dots all around the map

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.