Help with Root Motion (animating the HumanoidRootPart)

I’ve been working on making this more viable. I previously mentioned that:

This means we can drop these two lines of code and the animation will look normal on the client aswell.

    -- Code below removes root transform from the animation.
    -- In a final implementation you need to preprocess the animation so you don't use the two lines below.

	-- Remove joint translation. (Keep Y axis translation.)
	RootJoint.Transform -= RootJoint.Transform.Position * XZPlane

	-- Remove Y axis rotation from animation.
	RootJoint.Transform = GetTransform(Transform).Rotation:Inverse() * RootJoint.Transform

I haven’t made any tools but I have a technique to:

  • preprocess the animation
  • extract the root motion
  • add root motion back to character movement based on preprocessed animation playback

Step 1 - Extract root motion
We can extract root motion by converting the raw KeyframeSequence animation into a CurveAnimation using the animation editor and taking out the Vector3Curve and EulerRotationCurve generated for the root joint (highlighted in image below) and save them for later.
image

Step 2 - Preprocess animation, remove root translation / Y axis rotation
On the raw KeyframeSequence animation we want to remove the root translation and Y axis rotation. Using the module below, select your keyframe sequence and require() from the command bar within studio. Will go through all the poses in the animation and call GetProcessedTransform() on them. The animation will now appear as it does on “RootTransformRemoved”, upload the animation to roblox.

local module = {}


local XZPlane = Vector3.new(1, 0, 1)


local function GetProcessedTransform(Transform)

	local _, Heading, _ = Transform:ToEulerAngles(Enum.RotationOrder.YXZ)
	local Orientation = CFrame.fromEulerAngles(0, Heading, 0, Enum.RotationOrder.YXZ)

	Transform -= Transform.Position * XZPlane
	Transform = Orientation:Inverse() * Transform

	return Transform

end


local function Process(KeyframeSequence)

	local PoseCount = 0
	for _, Keyframe in pairs(KeyframeSequence:GetKeyframes()) do
		local Pose: Pose = Keyframe.HumanoidRootPart.LowerTorso
		--Pose.CFrame -= Pose.CFrame.Position * Vector3.new(1, 0, 1)
		Pose.CFrame = GetProcessedTransform(Pose.CFrame)
		PoseCount += 1
	end
	print("Processed", PoseCount, "poses.")

end


local function Run()

	if not game:GetService("RunService"):IsStudio() then
		print("Not studio.")
	end

	local KFS: KeyframeSequence = game.Selection:Get()[1]
	if KFS == nil or KFS:IsA("KeyframeSequence") == false then
		print("Not KeyframeSequence")
		return
	end

	Process(KFS)

end

Run()

return module

Step 3 - Play animation normally and move character based on extracted motion
Paste the extracted root transform curves under an animation instance with the AnimationId set to the processed animation.
image

New code will look like this:

local RunService = game:GetService("RunService")

local Humanoid = script.Parent.Humanoid
local Animator = Humanoid.Animator

Humanoid.AutoRotate = false

-- We now get animation from some container somewhere.
-- Extracted root motion "Vector3Curve" and "EulerRotationCurve" are children beneath animation.
local Animation = Humanoid.AerialEvade

local Track = Animator:LoadAnimation(Animation)
Track:Play(0, 1, 1)

-- Using this to wait for animation to load on client and then send a new time position.
-- If animation on client is not synced up with root part movement then it looks wrong.
-- I think the client just plays animtion from the same time position it received after loading the
-- animation without accounting for the time passed while animation was loading.
task.spawn(function()
	task.wait(6)
	Track.TimePosition = 0
end)

-- Dont need to get RootJoint.Transform anymore.
-- local RootJoint = script.Parent.LowerTorso.Root
local RootPart = Humanoid.RootPart


local function MoveHumanoid(Humanoid, Direction, dt)

	local Distance = Direction.Magnitude
	Humanoid.WalkSpeed = Distance / dt
	if Distance > 0 then
		Humanoid:Move(Direction.Unit, false)
	else
		Humanoid:Move(Vector3.zero, false)
	end

end

-- This function will get the original RootJoint.Transform at the TimePosition of the animation track.
local function GetTrackRootTransform(Track: AnimationTrack)

	-- Get the track animation.
	local Animation = Track.Animation

	-- Get the position and rotation curve which are children under the animation.
	local MotionCurvePosition: Vector3Curve = Animation:FindFirstChild("Position") -- A curve with the extracted root position of animation.
	local MotionCurveRotation: EulerRotationCurve = Animation:FindFirstChild("Rotation") -- A curve with extracted root rotation of animation.

	local TimePosition = Track.TimePosition

	-- Create the final transform cframe and return.
	local Translation = Vector3.new(unpack(MotionCurvePosition:GetValueAtTime(TimePosition)))
	local Rotation = MotionCurveRotation:GetRotationAtTime(TimePosition)

	return Rotation + Translation

end

-- I think the extracted root motion can be preprocessed so we don't need this function.
local function GetTransform(Transform)

	local _, Heading, _ = Transform:ToEulerAngles(Enum.RotationOrder.YXZ)
	local Orientation = CFrame.fromEulerAngles(0, Heading, 0, Enum.RotationOrder.YXZ)
	return Orientation + Transform.Position

end


local LastTransform = GetTrackRootTransform(Track) --RootJoint.Transform

Track.DidLoop:Connect(function()
	LastTransform = GetTrackRootTransform(Track) --RootJoint.Transform
end)

RunService.Stepped:Connect(function(_, dt)

    -- Instead of RootJoint.Transform we now get transform from extracted root transform.
    local Transform = GetTrackRootTransform(Track)

    local RelativeDelta = GetTransform(LastTransform):ToObjectSpace(GetTransform(Transform))

    LastTransform = Transform

    -- Make motion relative to character root part.
    local MoveMotion = RootPart.CFrame:VectorToWorldSpace(RelativeDelta.Position)
    MoveHumanoid(Humanoid, MoveMotion, dt)

    -- Rotate root part.
    RootPart.CFrame *= RelativeDelta.Rotation

    -- We don't remove the root joint translation and rotation here anymore.

end)

New model:

To use this model:

  • Upload the animation called ‘AerialEvadeProcessed’.
  • Change the AnimationId in ‘AerialEvade’ to your uploaded animation.
    image
18 Likes

Does the third step have to happen from the server?

I got your demo working with my animation but I’m having trouble integrating your code into my existing animation scripts, wondering if it’s because all my animation code is client-side.

The easiest method is pretty close to what OP was originally coming up with.
Use something like moon animator which lets you edit properties to edit the root’s CFrame (this is saved apart from the normal animation in JSON). Tween along those CFrames using the keyframe’s desired easing, and feed that into an AlignPosition and AlignOrientation both with RigidityEnabled, and on the client of the respective player, or server if it’s an NPC. This does have a few issues, as you have to create functionality beyond just playing like stopping, changing animation speed, or skipping to a specific frame in an animation yourself. I believe this is better than using :Move() as move doesn’t let you do vertical movement (I think?).

1 Like

can you show?

so with the moon animator stuff, use that info and use align position to move the player, align orientation to rotate, and play the anim all on the server?
if you dont mind showing me how you would do this in code you could dm me or just reply

1 Like

I apologize for reviving the thread, but after using your system, the animation appears to “stutter” when viewed on the client side, but smooth when looking through the server side. How can I fix this? I’ve been trying to come up with gimicky work arounds for the past 3-4 hours lol

Can you post a video of what it looks like?

Hey sorry for the late response but here is the video

Edit: I just realized my recorder didn’t really pick it up because of the quality, but basically on my end it was jittery, vs on the server end it was completely smooth

I guess the only real way to prevent this is by playing the animation on the client, but even for the moving of the player it would have to be done server side, which is just going to be jittery and inconsistent with the animation.

Edit: I just read through a forum post where the auther said they could update the CFrame of a BasePart globally via a LocalScript, because BasePart.Archivable = false. So maybe our solution here is to move everything in the Script to a LocalScript and set BasePart.Archivable = false? I’ll try it and tell you what happens.

Edit 2: Ok well I did it and it works! But it actually perfectly demonstrates the jittering I was talking about even though the movement and animation was done on the client.

The jitter is happening because the RootPart.CFrame property is being set every frame and is then being replicated over the network.

I did a quick test where instead of setting the CFrame property, I use an AlignOrientation to control the rotation of the character. This means the root part position will be replicated using pure physics.
image
This will preserve the smoothing of the replicated root part position. But it still has some issues.

The main issue is that rotating the character using AlignOrientation does not move them the exact amount required so that it lines up with the animation, even when RigidityEnabled = true.

You can also see in the video that the pure physics replication version is taking a lot of shortcuts when replicating the character position.

3 Likes

I’ve been trying to utilize this, but have been having a few pretty large problems.
For one, the character isn’t sent forward nearly as much as the original animation.

Second, when I try to use this on a player (via localscript), it seems to have an issue where the torso snaps to face directly forward at the beginning of the animation, which is strange as it doesn’t happen server-side on non-player characters.

The thing at the end of the swing is happening because i stop the root motion as soon as the animation finishes, as the idle animation just causes the character to spin around.

If possible, I want all of this to be handled on the client. Any help would be appreciated, as I’ve spent hours just trying to no avail.

… Also yes, I know this thread is a few years old now.

For your first problem where the character isn’t moving enough I don’t have any ideas without having more context (share how you implemented my code?).

For your second problem where the character rotation snaps at the start of the animation I have an idea for why that’s happening.

The character is rotated by finding the difference between the root transform on the current frame vs the last frame.

-- Get the delta between root transform last frame and this frame.
-- The GetTransform() function cleans up the transform a little bit so
-- that the result has Transform.YVector == Vector3.yAxis
-- (because we only care about rotation around the y axis, it's complicated)
local RelativeDelta = GetTransform(LastTransform):ToObjectSpace(GetTransform(Transform))
-- Rotate root part.
RootPart.CFrame *= RelativeDelta.Rotation

When the rotation snaps I think what is happening is that there is an old value in LastTransform which causes a snap on the first frame whenever you play your animation. What you can do to fix this is set LastTransform = Transform whenever you play the animation. Maybe that will fix the snapping issue.

As for the thing where your character slides back at the end of the animation, you have process the animation by extracting out the root motion and storing it elsewhere for later use.

See this post:

1 Like

The code I’m using is almost the exact same as the one posted here.

The only difference between the code posted there and mine is that I make a check so that the rotation is only happening if AutoRotate is off. (as shown below)

game:GetService("RunService").Stepped:Connect(function(_, dt)
	if Humanoid then
		if Humanoid.AutoRotate then return end

Strangely, for the snapping, it was the opposite issue, where setting it every time I played an animation caused the behavior. It still snaps to make the torso face forward while running, however.
as of now, here’s how it behaves:

and, just in case I missed anything, here’s a simple version of the system, put into a tool for testing. I included the animations, with only the torso keyframes.
test system.rbxm (16.3 KB)
^ Edit, Accidentally left a *2 on the Humanoid.WalkSpeed, which i was using to test the direction.
Thanks for the help!

Ok so the thing with the the character not moving probably has something to do with how the root joint for R6 has a strange orientation compared to R15. So my guess is that maybe WhiteCat didn’t adapt the script properly for R6. I don’t have a solution for this.

The next thing is that my code only works properly with one animation playing at a time, with that single animation resetting the last transform when it ends.

Track.DidLoop:Connect(function()
    -- This stops the character popping back to the start on the next frame.
    -- RootJoint.Transform at this point will be what the root transform of the
    -- animation is at Track.TimePosition = 0.
    LastTransform = RootJoint.Transform
end)

At this point in my response I got really stuck trying to think about how to explain why stuff doesn’t work blah blah…
But basically, you can’t play multiple animations that influence the root transform at the same time because they all get blended together in the root joint and you can’t determine what the last transform should be set to when an animation ends.

I think you should just work with R15 rigs for the time being while R6 doesn’t seem to work.
I also think you should try to do the animation preprocessing thing I mentioned, it will be necessary.

1 Like

i’ll definitely do the preprocessing once I can work out it’s other issues. unfortunately, “just using r15” won’t work for me, as i’ve already made quite a few animations. there’s always the hacky solution of just converting r6 animations over to r15, and then using a fake r6 body, but i’ll try to make this system work with r6 first.
thanks for the explanation. if i get this working, i’ll post my solution.

1 Like

I’m having an issue where for some reason if the root motion is done on the client the player doesn’t get moved nearly as far forward as they should, but if the root motion is done on the server the player is moved forward the correct amount.

Root motion done on client

Root motion done on server

Both the client and server are using the same module script so I dont think there should be any difference between the two.

Edit:

For some reason if you are holding a movement key (in the direction the animation is supposed to move you) the root motion works completely fine on the client

(Holding W while animation is playing)

Edit 2:

Ok I think I figured out the issue, the client has to use RenderStepped instead of Stepped for the root motion to match correctly. Not sure as to why this makes such a big difference but it works so.

Using RenderStepped

1 Like

Could you please explain step 2 further?

This step converts the animation of the character on the left to the animation playing on the character on the right.

The character is then moved in sync to the processed animation (right) using the motion that was extracted from the original animation (left), as seen on the character named “RootMotion” below.