Help with Root Motion (animating the HumanoidRootPart)

I think you are going to have to do some experimenting so see if you can for example do a simple arm movement like make a wave and see if that works.

That’s easy enough to do; just manipulate the Motor6D.Transform property based on the animation data.

I think I’ll just try to set the CFrame directly anyways despite the collision issue. I’ll just have to trust that the CFrame movements are small enough in my animation to have collision resolution work properly.

1 Like

Unfortunately there isn’t an engine feature for root motion. I’ve also tried searching Google, the DevForum, and the DevHub but have found nothing relating to a root motion feature on Roblox’s engine. Most I’ve found are some hacky solutions such as using body movers like you said or changing the position of the character to a position of one of the body parts at the end of the animation (source: How do you move the humanoidrootpart with an animation?) but I don’t recommend using them. There is a feature request for root motion already posted as well.

I see what you’re trying to go for by “animating the HumanoidRootPart directly”, but I’m not sure if you can actually do this. I haven’t tried myself though. Setting the HumanoidRootPart’s CFrame directly based on animation data wouldn’t work either as animations are relative to the HumanoidRootPart, so it would just keep sending you forward (I think; once again I haven’t actually tried this so feel free to correct me if I’m wrong).

You’d think this would’ve already been implemented as it’s a pretty essential feature. For my case, I’ve been working and planning on a baseball project and one of the main issues I’m encountering is how I’m going to go about animations (especially fielding animations). The best I can come up with is moving the character manually along a pre-defined path as the animation is playing and also adjust movement speed accordingly while the animation is playing. Obviously, this will require lots of trial and error. The animation itself would have to be in-place.

I still haven’t made a decision on what I’m going to do for the animations in my project but I might just end up dealing with with the fact that there’s no root motion feature and make the best out of it, unless I feel like torturing myself with the method I proposed.

It should be possible: just store the initial HumanoidRootPart CFrame as the reference point for your animation. I didn’t do that method anyways. If I did, I might as well have created the framework for a custom animation engine.

I used another rig to base the root motion on.

How this works is that:

  1. RootMotionRig PrimaryPart CFrame is set at the HumanoidRootParts initial CFrame
  2. HumanoidRootPart’s CFrame is set to RootMotionPoint’s Motor6D.Transform CFrame every frame

It works fine if you don’t care about collision. I implemented raycast collision, but it’s very primitive.

Or maybe you could do some fancy math involving the animation data, then feed that into :ApplyImpulse? Collision would be accounted for.
https://developer.roblox.com/en-us/api-reference/function/BasePart/ApplyImpulse

Decided to try out your method with the rig and I got very satisfactory results as you can see in this video.

What I did was I added two animations (or more like copied with slight variation) with a script which are both derived from the original animation, which is the running direction change as shown in the video.

One of the animations is for the root motion rig. The motion point’s transform CFrame is based on the LowerTorso’s CFrame, except orientation is only from side to side and movement is only on the X and Z axes. The Y value stays the same.

The second animation is basically the original animation but in-place. Lower torso only moves on the Y axis.

Play both animations at the same time and you get root motion.

3 Likes

Here’s what I’ve got after working on this problem for a little bit.

local RunService = game:GetService("RunService")

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

Humanoid.AutoRotate = false


local Animation = Instance.new("Animation")
Animation.AnimationId = "rbxassetid://10320606948" -- AerialEvade
-- You have to use your own animation which includes root transform.

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


local RootJoint = script.Parent.LowerTorso.Root
local RootPart = Humanoid.RootPart

local LastTransform = RootJoint.Transform
local LastOrientation = CFrame.identity

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

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


local function OrientationFromTransform(Transform)

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

end


Track.DidLoop:Connect(function()
	LastTransform = RootJoint.Transform
	LastOrientation = OrientationFromTransform(RootJoint.Transform)
end)

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

	local Transform = RootJoint.Transform

	local Orientation = OrientationFromTransform(Transform)
	local RelOrientation = LastOrientation:ToObjectSpace(Orientation)
	RootPart.CFrame *= RelOrientation

	local DeltaPos = Transform.Position - LastTransform.Position

	-- Make motion relative to character root part.
	local MoveMotion = (RootPart.CFrame * Orientation:Inverse()):VectorToWorldSpace(DeltaPos)
	MoveHumanoid(Humanoid, MoveMotion, dt)

	LastTransform = Transform	
	LastOrientation = Orientation

	-- Remove XZ translation.
	RootJoint.Transform -= RootJoint.Transform.Position * XZPlane

	-- Rotates entire animation from origin.
        -- It's fine that we rotate from origin since we just removed the XZ translation.
	RootJoint.Transform = Orientation:Inverse() * RootJoint.Transform

end)

This basically extracts the XZ translation (position) and Y axis orientation of the animation from the transform of the root joint. These two components are removed from the transform so the animation looks like the one playing on the character named “RootTransformRemoved”.

The extracted position and orientation are then applied back in a way that moves the root part.
The position changed by setting Humanoid.WalkSpeed and calling Humanoid:Move() to move the needed amount and the orientation is changed by setting the CFrame of the root part.

This is not a final implementation but it’s just a test which shows this is possible.
For a final implemetation you would need to upload the animations as they appear on the “RootTransformRemoved” character and have a system which applies movement and rotation from the root motion extracted from the original animations.
Someone will have to create some tools to process an animation and extract the root motion.

34 Likes

Best solution I’ve seen by far. I’ve thought of using :Move but didn’t know you could modulate the walkspeed like that. Thanks

1 Like

Here’s the simplified code which should be more easy to understand.

local RunService = game:GetService("RunService")

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

-- Stop character from rotating towards direction of movement.
Humanoid.AutoRotate = false

local Animation = Instance.new("Animation")
Animation.AnimationId = "rbxassetid://10320606948" -- AerialEvade, probably private, can't use.

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


local RootJoint = script.Parent.LowerTorso.Root
local RootPart = Humanoid.RootPart

local LastTransform = RootJoint.Transform

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

-- Move humanoid by a certain distance.
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 will basically remove the Y axis rotation of the transform.
-- Hard to explain.
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


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

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

	local Transform = RootJoint.Transform

	-- Get the delta (difference) between the transform this frame and last frame.
	-- Delta is relative to LastTransform.
	local RelativeDelta = GetTransform(LastTransform):ToObjectSpace(GetTransform(Transform))

	LastTransform = Transform

	-- Move the character by the delta (But make it move relative to current root part orientation).
	local MoveMotion = RootPart.CFrame:VectorToWorldSpace(RelativeDelta.Position)
	MoveHumanoid(Humanoid, MoveMotion, dt)

	-- Rotate the character by the delta.
	RootPart.CFrame *= RelativeDelta.Rotation

	-- 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

end)

5 Likes

Does this method only works with R15?

For R6 I believe the only change necessary would be to change the path of the RootJoint Motor6D instance.

local RootJoint = script.Parent.LowerTorso.Root -- From this.
local RootJoint = script.Parent.HumanoidRootPart["Root Hip"] -- To this.

You also have to upload your own R6 animation.

Animation.AnimationId = "rbxassetid://10320606948" --  Put your R6 animation here instead.

And remember that this is just a proof of concept. It won’t look correct from the clientside as the translation and rotation of the animation is only being removed from the server script.

1 Like

R6 rigs don’t seem to have a Motor6D named Root, would it be best to make a custom rig for that? The root Motor6D on an R6 rig is under the HumanoidRootPart.

I edited my comment to use the correct R6 root joint. I had no idea R6 naming and hierarchy was so wild.

Finally got around to playing around with this, and it works wonderfully with R15 rigs. Unfortunately R6 animations don’t play well because of the rotations placed on the Root Motor6D.C0 and C1.


In this video, SwordRig is a normal R6 rig, while SwordRig2’s Root Hip was edited to have its C0 and C1 set to zero. SwordRig2 works as it should. If you or anyone else has a solution, please let me know.

1 Like

Just needed to edit some values related to which axis the RootPart was rotating and translating on :hidere:

-- Example of how you can extract root motion from an animation and apply it to the character.
-- Code by @Razorter

local RunService = game:GetService("RunService")

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

-- Stop character from rotating towards direction of movement.
Humanoid.AutoRotate = false

local Animation = Instance.new("Animation")
Animation.AnimationId = "rbxassetid://" -- insert r6 anim here

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


local RootPart = Humanoid.RootPart
local RootJoint = RootPart["Root Hip"]

local LastTransform = RootJoint.Transform

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

-- Move humanoid by a certain distance.
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 will basically remove the Y axis rotation of the transform.
-- Hard to explain.
local function GetTransform(Transform)

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

end


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

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

	local Transform = RootJoint.Transform

	-- Get the delta (difference) between the transform this frame and last frame.
	-- Delta is relative to LastTransform.
	local RelativeDelta = GetTransform(LastTransform):ToObjectSpace(GetTransform(Transform))

	LastTransform = Transform

	-- Move the character by the delta (But make it move relative to current root part orientation).
	local MoveMotion = RootPart.CFrame:VectorToWorldSpace(Vector3.new(RelativeDelta.Position.Z, 0, RelativeDelta.Position.Y))
	MoveHumanoid(Humanoid, MoveMotion, dt)

	-- Rotate the character by the delta.
	local rx, ry, rz = RelativeDelta:ToOrientation()
	RootPart.CFrame *= CFrame.Angles(rx, rz, ry)

	-- 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

end)
10 Likes

the only problem with this is that the animation will have to be played server-side
with tools like a sword the animations are usually handled on client and the server handles damage

Why so? I don’t see the issue since the player has network ownership of their character anyway, but if you’re talking about security then that’s a whole other topic to delve into.

when an animation is played on the client, it will be smooth as it depends on your character and replicated to the server, however when its played on server it replicated to all clients and the smoothness of the animation could be alot worse depending on the characters connection

I feel like that’s just the unavoidable nature of server-client communication anyway if a player’s shoddy connection is causing animation lag. You do bring up a good point though, makes me wonder if this method has any potential desync between the root motion and animation.

shouldnt be any desync as both the anim and the root are handled on the server

1 Like

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
19 Likes