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.
Just needed to edit some values related to which axis the RootPart was rotating and translating on
-- 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)
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
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.
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.
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.
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?).
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
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.
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.
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:
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!