Por que meu rig está torcendo, esticando e travando em T-pose
Seu rig gira o braço em torno do torso, estica a perna pro outro lado do mapa ou trava em T-pose enquanto a animação roda lisa no Animation Editor. O defeito quase nunca está na animação: está no Motor6D do rig que está no Workspace, e dá pra apontar a junta culpada em dois segundos.
Rascunho assistido por IA, revisado, testado e editado por um humano antes de publicar. Ver Política Editorial. · Revisado por Gustavo Cantino, 20/09/2026 Política Editorial

Se o seu rig está "fazendo isso" — o braço girando em torno do torso inteiro, a perna esticando até o outro lado do mapa, a cabeça descolando do pescoço, o personagem plantado em T-pose enquanto a mesma animação roda lisa no Animation Editor —, ele não está bugado. Ele está obedecendo, à risca, a hierarquia de Motor6D que existe no Workspace agora. E essa hierarquia está diferente da que existia quando você gravou a animação.
Seu rig não está bugado, ele está obedecendo
Vale entender o mecanismo antes de sair mexendo em coisa. Uma animação do Roblox não guarda "o braço subiu 30 graus" amarrado a um objeto específico: ela é uma KeyframeSequence que guarda uma pose por nome de junta, dentro de uma hierarquia de nomes. Quando a track toca, o Animator procura no rig uma junta chamada LeftShoulder, aplica a transformação naquele Motor6D e segue em frente. Se essa junta tiver outro nome, apontar para outra parte, estiver com Part0 e Part1 invertidos ou simplesmente não existir, o Animator não solta erro nenhum no Output. Ele aplica onde encontra e ignora o resto. O espaguete que você está vendo é o rig fazendo o que foi mandado.
A parte cara é o que a maioria faz em seguida: assume que a animação está torta e volta pro Blender. O ciclo completo tem seis etapas — ajustar no Blender, exportar FBX, importar no Animation Editor, exportar a animação, publicar, esperar a moderação e trocar o asset ID nos scripts. Você repete isso três vezes, queima uma tarde inteira, gera três asset IDs novos que ficam pendurados no seu jogo pra sempre, e o rig continua torcendo. Porque o defeito nunca esteve no arquivo: estava no modelo que está dentro do Workspace.
O contorno numérico é simples e serve de primeiro teste. Um rig R15 padrão tem 16 partes e exatamente 15 Motor6D: Root, Waist, Neck, os três de cada braço (Shoulder, Elbow, Wrist) e os três de cada perna (Hip, Knee, Ankle). Um rig R6 tem 7 partes e 6 juntas: RootJoint, Neck, Left Shoulder, Right Shoulder, Left Hip e Right Hip — com espaço no nome, e todas parenteadas no Torso. Se o seu rig não bate com esses números, você já tem por onde começar antes de abrir qualquer software 3D.
Os cinco diagnósticos, na ordem certa
1. Conte as juntas e confira Part0 e Part1. Selecione o rig no Explorer, olhe quantos Motor6D existem e abra cada um dos suspeitos. Part0 é o pai (fica parado), Part1 é o filho (é o que gira). No LeftShoulder de um R15, Part0 tem que ser UpperTorso e Part1 tem que ser LeftUpperArm. Se estiver assim, o braço gira em torno do ombro. Se estiver ao contrário, o torso inteiro é que passa a girar em torno do braço — que é exatamente o "meu rig gira todo quando eu movo o braço" que você está vendo. O jeito errado de consertar, e que gente faz direto em tempo de execução:
local m = npc.UpperTorso.LeftShoulder
m.Part0 = npc.LeftUpperArm -- filho virou pai
m.Part1 = npc.UpperTorso
-- agora a animação inteira gravada nesse rig está deslocadaInverter Part0 e Part1 no runtime não "desfaz" nada: o C0 e o C1 continuam calculados pro sentido antigo, e você troca um bug por outro mais difícil de enxergar. Conserta no rig-fonte, no Studio, com o jogo parado.
2. Confira nome por nome, inclusive espaços e maiúsculas. Esse é o campeão silencioso. Você renomeou UpperTorso pra Torso_01 no Blender, ou o artista exportou com sufixo .001, ou alguém duplicou um membro e o Studio criou LeftUpperArm e LeftUpperArm com o mesmo nome. A animação continua procurando o nome antigo, não acha, e aquele membro fica congelado na pose neutra — daí o efeito "metade do corpo anima, metade fica em T-pose". Em R6 o erro clássico é gravar Left Shoulder sem o espaço. Nome de junta é string, e string não perdoa.
3. Anchored, Weld sobrando e RootPart. Se qualquer parte do rig estiver com Anchored marcado, o Motor6D não consegue mover ela, e todo o resto passa a orbitar aquela parte travada. Isso acontece direto quando você arrasta o rig pelo mapa e, pra ele não cair, ancorou um membro em vez do HumanoidRootPart. Na mesma linha: WeldConstraint ou Weld antigo entre duas partes que já têm um Motor6D ligando as duas. Os dois tentam resolver a mesma posição, o resultado é o membro tremendo ou colado no lugar errado. E confira se Model.PrimaryPart aponta pro HumanoidRootPart — sem isso, PivotTo e MoveTo arrastam o rig pelo pivô errado e você jura que a animação empurrou o personagem.
4. Prioridade e peso das tracks. Se a animação toca mas fica "piscando" com a idle, ou some depois de meio segundo, o problema é briga de prioridade. A ordem do enum AnimationPriority é Core, Idle, Movement, Action, Action2, Action3, Action4 — do mais fraco pro mais forte. O script Animate padrão do personagem toca idle e caminhada o tempo todo; se a sua animação de ataque foi exportada como Idle, ela empata com a idle padrão e o resultado é o personagem tremendo entre as duas poses. Animação de ação tem que ser Action pra cima. Dá pra forçar em código sem reexportar:
local animator = humanoid:FindFirstChildOfClass("Animator")
local track = animator:LoadAnimation(anim)
track.Priority = Enum.AnimationPriority.Action
track:Play(0.1) -- fade de 0.1s, o padrãoO outro lado dessa moeda é track esquecida tocando. Uma track que você nunca deu Stop continua somando peso em cima das outras. Se o personagem parece estar em duas poses ao mesmo tempo, conte quantas tracks estão tocando antes de culpar o rig.
5. Animator, e não Humanoid:LoadAnimation. Carregar a track direto no Humanoid está deprecado há anos. Ainda funciona em muitos casos, o que é justamente a armadilha: funciona no Studio em modo solo e falha ou replica errado no servidor de verdade. Carregue sempre pelo Animator que fica dentro do Humanoid. Para o personagem do próprio jogador, carregue no cliente e a replicação sai de graça; para NPC, carregue no servidor. Se não existe Animator dentro do Humanoid, nenhuma track vai tocar e o Output não vai te avisar.
Um sexto item que vale a checagem quando o membro aparece "descolado" do corpo: escala. Os valores de escala do Humanoid (altura, largura, profundidade, tamanho da cabeça) reescrevem os C0 das juntas quando o personagem nasce. Animação gravada num rig escalado e aplicada num rig padrão vai bater em offsets diferentes, e o resultado é mão flutuando a meio stud da manga.
O script que aponta a junta culpada em dois segundos
O artefato abaixo é um ModuleScript que faz essa varredura inteira sozinho. Ele compara o rig contra o mapa oficial de juntas R15 ou R6 (escolhido pelo RigType do Humanoid), acusa junta ausente, Part0 e Part1 invertidos, ligação apontando pra parte errada, nome duplicado, parte Anchored, Weld disputando com Motor6D, PrimaryPart vazio e Animator faltando. Cada linha devolvida traz gravidade, nome da junta e o que está errado — então você lê o Output e vai direto no Explorer consertar aquela junta específica.
Coloque como ModuleScript em ServerScriptService com o nome RigDoctor e chame assim, apontando pro modelo que está torcendo:
local RigDoctor = require(script.Parent.RigDoctor)
for _, p in RigDoctor.diagnosticar(workspace.NPC) do
print(p.gravidade, p.junta, p.mensagem)
end
for _, linha in RigDoctor.listarTracks(workspace.NPC) do
print(linha)
endA segunda função lista as tracks tocando agora com prioridade, peso e velocidade — é ela que resolve o caso "a animação toca mas fica brigando com a idle". O segundo artefato, o JSON, é o mesmo mapa de juntas R15 em formato de arquivo: use como referência de nomenclatura de bones no Blender antes de exportar, e o problema do item 2 deixa de existir na origem.
O erro mais caro: reexportar em vez de consertar o rig
O prejuízo não é o tempo do reexport, é o rastro que ele deixa. Cada animação publicada vira um asset ID novo e permanente, passa por moderação, e vai parar espalhada em quatro, cinco, dez scripts diferentes. Depois de três tentativas você tem três IDs quase iguais no jogo e ninguém lembra qual é o bom. Quando alguém troca o rig de novo daqui a dois meses, o ciclo recomeça.
A regra é: animação reexportada só resolve problema de pose; rig quebrado só se resolve no rig. Antes de abrir o Blender, rode o diagnóstico. Se ele acusar junta invertida, nome errado ou parte ancorada, o Blender não tem nada a ver com isso.
E tem o agravante clássico: "consertar" o C0 em tempo de execução pra compensar um Part0 invertido. Você acha o offset que faz aquela pose parecer certa, comita, e todas as outras animações daquele rig passam a ficar tortas — porque o C0 é comum a todas elas. Se você já fez isso, o caminho de volta é apagar as compensações, restaurar o Motor6D no sentido certo no rig-fonte e testar as animações uma por uma.
Mantenha um rig de referência guardado no ReplicatedStorage, intocado, e sempre grave animação nele. E centralize os asset IDs numa tabela única de módulo em vez de espalhar número solto pelos scripts: quando precisar trocar uma animação, você muda em um lugar.
O que medir depois
Rode o RigDoctor em todos os NPCs no boot do servidor e mande as linhas de gravidade ERRO pro Output. Se aparecer defeito em rig que já está publicado, você descobre antes do jogador — e descobre em qual NPC, com o nome da junta, sem abrir o Explorer de cada um.
Depois meça duas coisas. A primeira: quantas AnimationTracks estão tocando por personagem em pico de partida, usando a listagem de tracks. Track esquecida tocando é acúmulo silencioso e aparece primeiro como "animação estranha", depois como queda de fps. A segunda: o tempo do seu ciclo de reexport, cronometrado. Se ele passa de dez minutos, o diagnóstico antes do Blender se paga na primeira vez que você não precisa abrir o Blender.
Por último, pare de gravar animação em rig baixado do Toolbox. Grave sempre no rig que está no jogo — é a única forma de garantir que os nomes de junta batem antes de você exportar qualquer coisa.
Artefato pronto pra usar
--!strict
-- RigDoctor: varre um rig R6/R15 e devolve a lista de defeitos que fazem
-- a animacao torcer, esticar ou travar em T-pose.
-- Nao conserta nada: aponta a junta culpada e o que esta errado nela.
export type Problema = {
gravidade: string, -- "ERRO", "AVISO" ou "OK"
junta: string,
mensagem: string,
}
-- Mapa oficial: [nome da junta] = { Part0 esperado, Part1 esperado }
local JUNTAS_R15: { [string]: { string } } = {
Root = { "HumanoidRootPart", "LowerTorso" },
Waist = { "LowerTorso", "UpperTorso" },
Neck = { "UpperTorso", "Head" },
LeftShoulder = { "UpperTorso", "LeftUpperArm" },
LeftElbow = { "LeftUpperArm", "LeftLowerArm" },
LeftWrist = { "LeftLowerArm", "LeftHand" },
RightShoulder = { "UpperTorso", "RightUpperArm" },
RightElbow = { "RightUpperArm", "RightLowerArm" },
RightWrist = { "RightLowerArm", "RightHand" },
LeftHip = { "LowerTorso", "LeftUpperLeg" },
LeftKnee = { "LeftUpperLeg", "LeftLowerLeg" },
LeftAnkle = { "LeftLowerLeg", "LeftFoot" },
RightHip = { "LowerTorso", "RightUpperLeg" },
RightKnee = { "RightUpperLeg", "RightLowerLeg" },
RightAnkle = { "RightLowerLeg", "RightFoot" },
}
local JUNTAS_R6: { [string]: { string } } = {
RootJoint = { "HumanoidRootPart", "Torso" },
Neck = { "Torso", "Head" },
["Left Shoulder"] = { "Torso", "Left Arm" },
["Right Shoulder"] = { "Torso", "Right Arm" },
["Left Hip"] = { "Torso", "Left Leg" },
["Right Hip"] = { "Torso", "Right Leg" },
}
local RigDoctor = {}
local function novo(gravidade: string, junta: string, mensagem: string): Problema
return { gravidade = gravidade, junta = junta, mensagem = mensagem }
end
-- Varre o rig e devolve todos os defeitos encontrados.
function RigDoctor.diagnosticar(modelo: Model): { Problema }
local problemas: { Problema } = {}
local humanoid = modelo:FindFirstChildOfClass("Humanoid") :: Humanoid?
if humanoid == nil then
table.insert(problemas, novo("ERRO", "-", "Modelo sem Humanoid: nao existe Animator e nenhuma track vai tocar."))
return problemas
end
if humanoid:FindFirstChildOfClass("Animator") == nil then
table.insert(problemas, novo("ERRO", "-", "Humanoid sem Animator. Crie um Animator dentro do Humanoid."))
end
if modelo.PrimaryPart == nil then
table.insert(problemas, novo("AVISO", "-", "PrimaryPart vazio: PivotTo/MoveTo vao arrastar o rig pelo pivo errado."))
end
local esperado = if humanoid.RigType == Enum.HumanoidRigType.R15 then JUNTAS_R15 else JUNTAS_R6
-- Indexa Motor6D por nome, conta duplicados e acusa Anchored/Weld no caminho.
local motores: { [string]: Motor6D } = {}
local contagem: { [string]: number } = {}
for _, d in modelo:GetDescendants() do
if d:IsA("Motor6D") then
contagem[d.Name] = (contagem[d.Name] or 0) + 1
motores[d.Name] = d
elseif d:IsA("BasePart") and d.Anchored then
table.insert(problemas, novo("ERRO", d.Name, "Parte Anchored: a animacao nao consegue mover essa parte."))
elseif d:IsA("Weld") or d:IsA("WeldConstraint") then
table.insert(problemas, novo("AVISO", d.Name, "Weld/WeldConstraint dentro do rig disputando posicao com o Motor6D."))
end
end
for nome, quantidade in contagem do
if quantidade > 1 then
table.insert(problemas, novo("ERRO", nome, string.format("%d Motor6D com o mesmo nome: a animacao so enxerga um deles.", quantidade)))
end
end
local conferidas = 0
for nome, par in esperado do
conferidas += 1
local m = motores[nome]
if m == nil then
table.insert(problemas, novo("ERRO", nome, "Junta ausente. A pose e gravada por nome de junta, entao esse membro fica em T-pose."))
continue
end
local p0 = m.Part0
local p1 = m.Part1
if p0 == nil or p1 == nil then
table.insert(problemas, novo("ERRO", nome, "Part0 ou Part1 vazio."))
continue
end
if p0.Name == par[2] and p1.Name == par[1] then
table.insert(problemas, novo("ERRO", nome, string.format("Part0/Part1 invertidos. Esperado %s -> %s.", par[1], par[2])))
elseif p0.Name ~= par[1] or p1.Name ~= par[2] then
table.insert(problemas, novo("ERRO", nome, string.format("Ligacao errada: %s -> %s. Esperado %s -> %s.", p0.Name, p1.Name, par[1], par[2])))
elseif m.Parent ~= p0 then
table.insert(problemas, novo("AVISO", nome, "Motor6D nao esta parenteado no Part0 (funciona, mas foge do padrao do rig)."))
end
end
for nome in motores do
if esperado[nome] == nil then
table.insert(problemas, novo("AVISO", nome, "Junta fora do padrao: confira se a animacao foi gravada com esse nome exato."))
end
end
if #problemas == 0 then
table.insert(problemas, novo("OK", "-", string.format("Rig %s integro: %d juntas conferidas.", humanoid.RigType.Name, conferidas)))
end
return problemas
end
-- Lista as tracks tocando agora: resolve o caso "a animacao briga com a idle".
function RigDoctor.listarTracks(modelo: Model): { string }
local linhas: { string } = {}
local humanoid = modelo:FindFirstChildOfClass("Humanoid") :: Humanoid?
local animator = if humanoid ~= nil then humanoid:FindFirstChildOfClass("Animator") :: Animator? else nil
if animator == nil then
table.insert(linhas, "Sem Animator: nenhuma track pode tocar.")
return linhas
end
for _, track in animator:GetPlayingAnimationTracks() do
table.insert(linhas, string.format(
"%s | prioridade=%s | peso=%.2f | velocidade=%.2f | loop=%s",
track.Name, track.Priority.Name, track.WeightCurrent, track.Speed, tostring(track.Looped)
))
end
if #linhas == 0 then
table.insert(linhas, "Nenhuma track tocando neste instante.")
end
return linhas
end
return RigDoctor{
"rigType": "R15",
"partCount": 16,
"motor6dCount": 15,
"note": "Part0 = pai (fica parado), Part1 = filho (gira). O Motor6D mora dentro do Part0.",
"joints": [
{ "name": "Root", "part0": "HumanoidRootPart", "part1": "LowerTorso" },
{ "name": "Waist", "part0": "LowerTorso", "part1": "UpperTorso" },
{ "name": "Neck", "part0": "UpperTorso", "part1": "Head" },
{ "name": "LeftShoulder", "part0": "UpperTorso", "part1": "LeftUpperArm" },
{ "name": "LeftElbow", "part0": "LeftUpperArm", "part1": "LeftLowerArm" },
{ "name": "LeftWrist", "part0": "LeftLowerArm", "part1": "LeftHand" },
{ "name": "RightShoulder", "part0": "UpperTorso", "part1": "RightUpperArm" },
{ "name": "RightElbow", "part0": "RightUpperArm", "part1": "RightLowerArm" },
{ "name": "RightWrist", "part0": "RightLowerArm", "part1": "RightHand" },
{ "name": "LeftHip", "part0": "LowerTorso", "part1": "LeftUpperLeg" },
{ "name": "LeftKnee", "part0": "LeftUpperLeg", "part1": "LeftLowerLeg" },
{ "name": "LeftAnkle", "part0": "LeftLowerLeg", "part1": "LeftFoot" },
{ "name": "RightHip", "part0": "LowerTorso", "part1": "RightUpperLeg" },
{ "name": "RightKnee", "part0": "RightUpperLeg", "part1": "RightLowerLeg" },
{ "name": "RightAnkle", "part0": "RightLowerLeg", "part1": "RightFoot" }
]
}

