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.