Rig com a face virada pro lado errado no Roblox Studio
Seu modelo importado olha pra um lado e o HumanoidRootPart olha pro outro, então o NPC anda de lado e a mira sai pela orelha. Descubra em qual das três camadas o desvio foi gravado — root, Motor6D ou keyframe — e corrija com um módulo que gira o root sem mover a geometria.
Rascunho assistido por IA, revisado, testado e editado por um humano antes de publicar. Ver Política Editorial. · Revisado por Gustavo Cantino, 03/09/2026 Política Editorial
Seu modelo está com a face virada pro lado errado e o conserto não é girar ele no Studio até parecer certo. No Roblox, a frente de qualquer CFrame é o eixo -Z: o que o HumanoidRootPart chama de frente é o LookVector, e ele aponta pro -Z, sempre. O seu problema é que a geometria importada olha pra um lado e o -Z do root olha pra outro — e essa diferença foi gravada em uma de três camadas: no root, nos Motor6D ou nos keyframes da animação. Descubra em qual, conserte lá, e salve o rig corrigido como origem em vez de remendar cada cópia.
Onde essa diferença de ângulo custa caro
Enquanto o root e a geometria discordam, tudo que lê direção no seu jogo lê errado. Humanoid:MoveTo gira o root e deixa o AutoRotate alinhar o personagem: se a geometria está 90° torta, o NPC caminha de lado o percurso inteiro. O raycast de mira que sai de HRP.CFrame.LookVector aponta pra parede enquanto o boneco encara o jogador. O billboard que você posicionou em CFrame.new(0, 0, -3) nasce na nuca. Animações com root motion deslocam o personagem no eixo errado. Nada disso é bug de script — é o mesmo desvio se propagando por cinco sistemas.
O segundo custo é o conserto por instância. Selecionar o modelo, rotacionar na mão, dar Play, ver que voltou, reposicionar: são uns 3 a 5 minutos por modelo, com atenção. Em 40 NPCs isso dá entre 2h e 3h20 — e o trabalho inteiro evapora na próxima vez que o artista reexportar o FBX, porque o desvio está no arquivo de origem, não na sua cena.
Tem um sintoma que confunde muita gente e vale nomear: você gira as partes do modelo em modo edição, salva, dá Play e o boneco volta a ficar torto. Isso não é o Studio te sabotando. É o Humanoid reavaliando a cadeia de Motor6D no primeiro frame e recolocando cada parte onde o C0 manda. Girar BasePart num rig com juntas é rabiscar por cima de um dado que vai ser sobrescrito.
Antes de mexer em qualquer coisa, tire a medida no Command Bar:
local npc = workspace.Zumbi
print(npc:GetPivot().LookVector)
print(npc.PrimaryPart and npc.PrimaryPart.Name or "SEM PrimaryPart")Se o PrimaryPart volta vazio ou aponta pra uma perna, você já achou metade do problema: o pivô do modelo é uma coisa e o root do rig é outra.
Descubra qual das três camadas está errada
1. Materialize o -Z. Direção é difícil de julgar no olho. Cole isto no Command Bar com o modelo no Workspace e olhe onde o cubo nasce:
local m = workspace.Zumbi
local p = Instance.new("Part")
p.Size = Vector3.new(1, 1, 1)
p.Anchored, p.CanCollide = true, false
p.Color = Color3.fromRGB(255, 0, 0)
p.CFrame = m:GetPivot() * CFrame.new(0, 0, -4)
p.Parent = workspaceCubo na frente do rosto: camada 1 está certa. Cubo na orelha: desvio de 90°. Cubo na nuca: 180°. Anote o valor, ele vai ser o parâmetro do conserto.
2. Camada 1 — root contra geometria. É o caso mais comum em modelo importado. O jeito certo de corrigir é girar o root em torno de Y e compensar o mesmo ângulo, invertido, no C0 de todos os Motor6D que saem dele. O mundo continua exatamente igual, mas o -Z do root passa a apontar pra onde o rosto olha. É uma operação de dois passos que a mão erra e o script acerta — é exatamente o que o módulo da próxima seção faz.
3. Camada 2 — os Motor6D das juntas. Sintoma: o cubo de teste nasce certinho na frente, mas o boneco entorta assim que a Animation toca — braço pra trás, cabeça de lado, perna invertida. O C0 de cada Motor6D é a posição e a rotação da junta no espaço do Part0, e uma animação guarda rotações relativas a essa junta, não posições no mundo. Se você animou num rig cujo C0 do ombro tinha 90° de sobra e tocou num rig com o C0 padrão, o braço sai torto e nenhuma rotação no root conserta. Compare os dois rigs:
local a = game.ServerStorage.RigAnimado.UpperTorso.LeftShoulder
local b = workspace.Zumbi.UpperTorso.LeftShoulder
print(a.C0)
print(b.C0)Se os números divergem, a saída barata é reimportar a animação usando o mesmo rig em que ela foi feita, e não reescrever C0 em runtime.
4. Camada 3 — os keyframes. Rig alinhado em T-pose, mas na primeira frame da animação ele gira 90° e fica assim até acabar. Aí o offset está gravado dentro do arquivo de animação, na track do root. Você tem dois caminhos: abrir o Animation Editor e corrigir a track, keyframe por keyframe, ou voltar no Blender, arrumar a orientação e reexportar. Numa animação de 2 segundos a 30 fps são 60 frames por track — o caminho previsível é reexportar. E lembre: republicar gera um AnimationId novo. O antigo continua no ar e o seu script continua tocando o antigo até você trocar o ID na mão. Essa é a etapa que todo mundo esquece e depois jura que a correção não funcionou.
5. Blender, que é onde o desvio nasce. Se você rotacionou o objeto em Object Mode e não aplicou, o Blender guarda essa rotação como transform pendente e o exportador FBX resolve isso de um jeito diferente dependendo das opções. Faça Ctrl+A e aplique Rotation & Scale antes de exportar, ou rotacione a malha em Edit Mode, onde não sobra transform nenhum. O Blender é Z-up e o Roblox é Y-up: as opções Forward e Up do exportador cuidam dessa conversão de eixo vertical, mas nenhuma delas adivinha pra que lado o seu personagem foi modelado. Faça o teste do eixo uma vez — um cone colado no nariz, exporta, importa, olha — e grave o resultado num preset ao lado do .blend, porque essa é uma informação que você vai querer daqui a seis meses.
6. Salve a origem, não a cópia. Corrigido o rig no Workspace, publique ele de novo como asset ou salve o .rbxm e substitua o modelo em ServerStorage. Se a correção mora só na instância que está na cena, o próximo Clone() traz o problema de volta.
O módulo que detecta o desvio e corrige o root
O artefato abaixo é um ModuleScript que faz as duas coisas que a mão erra: medir o ângulo e aplicar a correção compensando os Motor6D. Ele descobre a frente visual pela posição de um Attachment — aceita um chamado FrontMarker, que você cria, ou o FaceFrontAttachment, que já existe na Head de qualquer rig R15. Com isso ele calcula o ângulo entre o -Z do root e a frente real, gira o root pelo negativo desse ângulo e reposiciona as partes a partir da cadeia de juntas, porque em modo edição o Studio não reassenta nada sozinho.
Coloque o arquivo em ServerScriptService com o nome RigOrientation e rode no Command Bar, com o rig já no Workspace:
local RO = require(game.ServerScriptService.RigOrientation)
print(RO.audit(workspace.Zumbi))
print(RO.fix(workspace.Zumbi, true))O true do segundo parâmetro arredonda o desvio para o múltiplo de 90 mais próximo — use quando o marcador não está perfeitamente centrado e a medida sai em 88,4°. Sem ele, a correção é exata, ao décimo de grau. O audit não altera nada e serve para varrer uma pasta inteira antes de decidir o que consertar. Depois de rodar o fix, refaça o teste do cubo vermelho: ele tem que nascer na frente do rosto.
O erro mais caro: maquiar o desvio em runtime
O remendo que parece resolver e cobra caro depois é esse:
-- ERRADO: offset colado por cima do erro
npc.PrimaryPart.CFrame = alvo.CFrame * CFrame.Angles(0, math.rad(90), 0)Funciona no teste e quebra em quatro lugares. Quebra quando o Humanoid com AutoRotate ligado reescreve o CFrame do root no mesmo frame. Quebra quando o personagem senta num Seat, que impõe o próprio alinhamento. Quebra quando a animação tem root motion. E, principalmente, não conserta nada do que lê direção: a mira, o campo de visão do NPC, o prompt e o billboard continuam lendo o LookVector errado, porque o LookVector continua errado — você só girou a aparência.
Tem o custo de máquina também. Se o remendo virou uma correção por frame, com 200 NPCs a 60 Hz você está fazendo 12.000 chamadas de PivotTo por segundo, todas replicando do servidor pros clientes, só pra disfarçar um dado que estaria certo com uma correção única no asset.
A saída é a ordem inversa: apague o offset do código primeiro, deixe o modelo ficar visivelmente torto de novo, e só então corrija na camada certa. Enquanto o remendo estiver ligado, você não consegue medir se o conserto de verdade funcionou.
O que medir depois
Bota o audit pra rodar num loop sobre todos os rigs de ServerStorage e conta quantos voltam com desvio maior que 1°. Esse número é a sua régua: ele precisa ir a zero e ficar em zero. Um warn no início do servidor listando os rigs desalinhados custa alguns milissegundos no boot e pega o próximo FBX torto no dia em que ele entra, não três semanas depois quando o QA reclama que o NPC anda de lado.
Depois disso, coloque o FrontMarker no rig base do projeto, aquele de onde todos os outros são derivados. Com o attachment lá, qualquer modelo novo é auditável em um comando, sem ninguém precisar olhar pro boneco e adivinhar o ângulo.
Por fim, faça a conta que decide o resto do trabalho: liste quantos AnimationId foram exportados a partir do rig torto. Cada um deles precisa de um ciclo de reexportar, reimportar, publicar e trocar o ID no script. Se forem três animações, você resolve na mesma tarde. Se forem trinta, corrija a camada 1 no rig base agora e programe a refação das animações como tarefa própria, com a lista de IDs antigos e novos escrita antes de começar — porque errar um ID no meio da troca é o jeito mais fácil de reintroduzir o mesmo bug com outro nome.
Artefato pronto pra usar
--!strict
-- RigOrientation
-- ModuleScript em ServerScriptService.
-- Convencao do Roblox: a frente de qualquer CFrame e o eixo -Z (CFrame.LookVector).
--
-- Uso no Command Bar do Studio, em modo edicao, com o rig no Workspace:
-- local RO = require(game.ServerScriptService.RigOrientation)
-- print(RO.audit(workspace.Zumbi))
-- print(RO.fix(workspace.Zumbi, true)) -- true = arredonda para o multiplo de 90 mais proximo
local RigOrientation = {}
-- Attachments aceitos como "aqui e a frente visual do modelo".
-- FaceFrontAttachment ja existe na Head de qualquer rig R15.
local FRONT_MARKERS: { string } = { "FrontMarker", "FaceFrontAttachment" }
local function getRoot(model: Model): BasePart
local hrp = model:FindFirstChild("HumanoidRootPart")
if hrp and hrp:IsA("BasePart") then
return hrp
end
local primary = model.PrimaryPart
if primary then
return primary
end
error("[RigOrientation] " .. model:GetFullName() .. " nao tem HumanoidRootPart nem PrimaryPart")
end
local function findFrontMarker(model: Model): Attachment?
for _, name in FRONT_MARKERS do
local found = model:FindFirstChild(name, true)
if found and found:IsA("Attachment") then
return found
end
end
return nil
end
-- Desvio em graus entre o -Z do root e a frente visual marcada pelo attachment.
-- Positivo = a frente real esta a direita do LookVector, visto de cima.
function RigOrientation.detectYawError(model: Model, snapTo90: boolean?): number
local root = getRoot(model)
local marker = findFrontMarker(model)
if not marker then
error("[RigOrientation] nenhum FrontMarker ou FaceFrontAttachment em " .. model.Name)
end
local localPos = root.CFrame:PointToObjectSpace(marker.WorldPosition)
local flat = Vector3.new(localPos.X, 0, localPos.Z)
if flat.Magnitude < 0.05 then
error("[RigOrientation] marcador em cima do eixo do root; mova ele para a frente da geometria")
end
local yaw = math.deg(math.atan2(flat.X, -flat.Z))
if snapTo90 then
yaw = math.round(yaw / 90) * 90
end
return yaw
end
-- Gira o root em torno de Y SEM mover a geometria: compensa nos Motor6D que saem dele.
-- Retorna quantas juntas foram compensadas.
function RigOrientation.applyRootYaw(model: Model, degrees: number): number
local root = getRoot(model)
local yaw = CFrame.Angles(0, math.rad(degrees), 0)
local touched = 0
root.CFrame = root.CFrame * yaw
for _, d in model:GetDescendants() do
if d:IsA("Motor6D") then
if d.Part0 == root then
d.C0 = yaw:Inverse() * d.C0
touched += 1
elseif d.Part1 == root then
warn("[RigOrientation] " .. d:GetFullName() .. " usa o root como Part1; confira na mao")
end
end
end
if touched == 0 then
warn("[RigOrientation] nenhum Motor6D sai do root; se for peca solta, ajuste Model.WorldPivot")
end
return touched
end
-- Em modo edicao o Studio nao reassenta as partes: esta funcao percorre a cadeia de Motor6D
-- e recoloca cada parte onde C0/C1 mandam.
function RigOrientation.reseat(model: Model): ()
local root = getRoot(model)
local motors: { Motor6D } = {}
for _, d in model:GetDescendants() do
if d:IsA("Motor6D") then
table.insert(motors, d)
end
end
local placed: { [BasePart]: boolean } = {}
placed[root] = true
local moved = true
while moved do
moved = false
for _, m in motors do
local p0, p1 = m.Part0, m.Part1
if p0 and p1 and placed[p0] and not placed[p1] then
p1.CFrame = p0.CFrame * m.C0 * m.C1:Inverse()
placed[p1] = true
moved = true
end
end
end
end
export type Report = {
root: string,
markerFound: boolean,
yawError: number,
motorCount: number,
hasHumanoid: boolean,
}
-- Somente leitura: nao altera nada. Bom para varrer uma pasta inteira.
function RigOrientation.audit(model: Model): Report
local root = getRoot(model)
local motorCount = 0
for _, d in model:GetDescendants() do
if d:IsA("Motor6D") then
motorCount += 1
end
end
local yaw = 0
local ok, result = pcall(function()
return RigOrientation.detectYawError(model, false)
end)
if ok and typeof(result) == "number" then
yaw = result
end
return {
root = root.Name,
markerFound = ok,
yawError = yaw,
motorCount = motorCount,
hasHumanoid = model:FindFirstChildOfClass("Humanoid") ~= nil,
}
end
-- Mede, corrige e reassenta. Retorna o angulo aplicado (0 = ja estava alinhado).
function RigOrientation.fix(model: Model, snapTo90: boolean?): number
local yaw = RigOrientation.detectYawError(model, snapTo90)
if math.abs(yaw) < 0.5 then
return 0
end
RigOrientation.applyRootYaw(model, -yaw)
RigOrientation.reseat(model)
return -yaw
end
return RigOrientation
{
"rigName": "npc_base_r15",
"engine": { "up": "Y", "front": "-Z", "unit": "stud" },
"blenderExport": {
"format": "FBX",
"forwardAxis": "-Z Forward",
"upAxis": "Y Up",
"applyTransform": true,
"applyScalings": "FBX All",
"applyRotationScaleInObjectMode": true,
"bakeAnimation": false
},
"checklist": [
"Ctrl+A > Rotation & Scale em Object Mode antes de exportar",
"Attachment FrontMarker 2 studs a frente da Head no rig base",
"Depois do import, RigOrientation.audit deve retornar yawError entre -0.5 e 0.5",
"Toda animacao exportada a partir deste rig, e so deste rig"
],
"lastVerifiedYawError": 0,
"animationIds": []
}