Manchas pretas e sombra em bloco no rig animado
Seu personagem animado fica com faces pretas em certos frames, a sombra anda em bloco atrasada e um braço acende e apaga perto de uma luz. São três causas diferentes — normal invertida no FBX, grid voxel de 4x4x4 studs e CastShadow em parte invisível — e este guia separa cada uma em quatro checagens de 30 segundos.
Rascunho assistido por IA, revisado, testado e editado por um humano antes de publicar. Ver Política Editorial. · Revisado por Gustavo Cantino, 04/09/2026 Política Editorial
Se o teu personagem animado fica com manchas pretas em alguns frames, se a sombra dele anda atrasada e em bloco quando ele caminha, ou se um braço acende e apaga ao passar perto de um PointLight, você não tem um "weird lighting bug" — você tem três bugs diferentes que se parecem. O primeiro é normal invertida vinda do Blender e não tem absolutamente nada a ver com o serviço Lighting. O segundo é o grid de iluminação voxel, que resolve luz em células de 4x4x4 studs enquanto o teu rig se move em frações de stud por frame. O terceiro é CastShadow ligado numa parte invisível ou uma luz com Shadows presa a um osso que anima.
A parte cara não é o bug: é o tempo que você queima procurando no lugar errado. O caminho padrão é abrir o Lighting, mexer em Brightness, adicionar ColorCorrection, trocar Skybox, testar Ambient, e duas horas depois o mesh continua com a mesma face preta — porque a face preta está no arquivo FBX, não na cena.
O bug não é um bug só: são três
Sintoma A — mancha preta que muda com o ângulo da câmera. Uma parte do mesh (quase sempre um braço, uma perna ou uma peça espelhada) fica escura como se estivesse dentro de uma caverna. Se você girar a câmera, a mancha muda de lugar ou some. Isso é normal apontando para dentro: a face está sendo iluminada pelas costas. Nenhum valor de Brightness conserta, porque a superfície está pedindo luz do lado errado.
Sintoma B — sombra que anda em degraus. O personagem caminha suave, a sombra no chão pula em quadrados e chega meio segundo depois. Isso é Lighting.Technology em Voxel ou Compatibility. O voxel resolve iluminação numa grade de 4x4x4 studs; um rig R15 padrão tem cerca de 5 studs de altura, então cada passo cruza borda de célula e a sombra recalcula em bloco. Skinned mesh nesse modo nem projeta a silhueta real — projeta aproximação de caixa.
Sintoma C — flicker por frame. Uma peça do corpo (ou a arma na mão) pisca entre iluminada e escura durante a animação. Duas causas comuns: luz com Shadows = true parenteada num osso que se move, forçando recálculo de sombra a cada frame; ou parte com Transparency = 1 e CastShadow = true — a hitbox invisível projeta uma sombra de caixa que não acompanha a malha deformada e briga com a sombra real.
Os três sintomas aparecem juntos no mesmo rig com frequência, e é por isso que parece "aleatório". Não é. Cada um tem um teste que isola em menos de um minuto.
Diagnóstico em quatro checagens de 30 segundos
Checagem 1 — é luz ou é malha? Com o jogo parado no Studio, selecione Lighting no Explorer e alterne Technology entre Voxel e Future no painel Properties. Olhe a mancha. Se ela mudar de forma, intensidade ou sumir, o problema é iluminação (sintomas B e C). Se ela continuar idêntica em todos os modos, o problema é a geometria — pule direto para a checagem 2. Detalhe que economiza tempo: Technology não é confiável de alterar por script em runtime, é uma propriedade de cena. Mude no painel, não no código.
Checagem 2 — teste da luz chapada. Deixe Lighting assim por 30 segundos: Ambient em 255,255,255, OutdoorAmbient em 255,255,255, Brightness em 0 e GlobalShadows desligado. Nessa configuração não existe direção de luz, então nada pode ficar escuro por sombra. Se ainda houver face preta, a normal está invertida ou o mesh veio com custom split normals quebradas. Confirme trocando o Material da parte para SmoothPlastic e a Color para branco — se a face segue preta, é normal, ponto final.
O conserto é no Blender, nesta ordem: Object Mode, Ctrl+A e aplicar All Transforms (isso é o que resolve escala negativa de espelhamento); Edit Mode, A para selecionar tudo, Shift+N para recalcular normais para fora; em Object Data Properties, remover Custom Split Normals Data se existir; na exportação FBX, deixar Smoothing em Face, Apply Modifiers ligado e Add Leaf Bones desligado. Reimporte e refaça a checagem 2 antes de tocar em qualquer outra coisa.
Um cuidado aqui: DoubleSided no MeshPart faz a mancha sumir e por isso muita gente para nesse ponto. Não é conserto, é curativo — você passa a renderizar as duas faces de cada triângulo e paga o custo em toda cópia do personagem no servidor. Conserte a normal e desligue.
Checagem 3 — a sombra em degraus. Se a checagem 1 apontou iluminação e o sintoma é sombra atrasada, o alvo é Technology. ShadowMap e Future usam shadow map real e resolvem silhueta de skinned mesh; Voxel e Compatibility não. Em Future você ganha também ShadowSoftness, que não existe nos outros modos. Mantenha ShadowSoftness baixo em cena com rig animado: valores altos deixam a borda da sombra "fervendo" durante a animação, porque a penumbra é recalculada com o corpo em movimento e o ruído aparece como cintilação.
Ainda nesta checagem, olhe EnvironmentDiffuseScale. Se estiver em 0 numa cena interna sem luz direta, o rig fica quase preto e parece bug de material. Não é: é a cena sem luz ambiente indireta.
Checagem 4 — o flicker. Selecione o rig e procure duas coisas: partes com Transparency em 1 e CastShadow ligado (HumanoidRootPart, hitboxes, attachments de colisão) e instâncias de luz com Shadows ligado dentro da hierarquia animada. Desligue CastShadow das invisíveis — parte invisível não deveria projetar nada, e a sombra de caixa dela é justamente o retângulo estranho que você vê no chão. Nas luzes presas a osso, deixe Shadows ligado em no máximo uma por personagem e reduza Range: luz de range alto que se move amplia o pop de atualização e o custo por frame.
As quatro checagens juntas levam menos de cinco minutos. É a diferença entre resolver hoje e passar a tarde no ColorCorrection.
O script de auditoria do rig
O artefato abaixo é um ModuleScript chamado LightingDoctor. Ele faz a checagem 4 inteira de forma automática e ainda te avisa quando a cena está em Voxel ou com ShadowSoftness alto demais para rig animado.
Coloque em ServerScriptService com o nome exato LightingDoctor. No Command Bar do Studio, com o rig selecionado no Explorer:
local D = require(game.ServerScriptService.LightingDoctor)
D.imprimir(game:GetService("Selection"):Get()[1])A saída lista cada achado com um código curto, o caminho completo da instância e o motivo. Códigos que aparecem mais: SOMBRA_FANTASMA (parte invisível projetando sombra), LUZ_COM_SOMBRA (luz animada recalculando por frame), DOUBLE_SIDED (curativo de normal invertida) e TECH_VOXEL (a cena inteira no modo errado).
Quando quiser aplicar as correções seguras — desligar CastShadow de parte invisível e Shadows de luz dentro do rig — troque para D.corrigir(...), que devolve quantas propriedades foram alteradas. Ele nunca mexe em DoubleSided, Material ou Technology: desligar DoubleSided sem consertar a normal antes revela buraco no mesh, e as outras duas são decisão de arte, não de auditoria.
O módulo é --!strict e não depende de nada além de Lighting, então funciona igual em Studio e em runtime. Rodar no Command Bar antes de exportar o personagem para o jogo custa 5 segundos por rig.
O erro mais caro: espelhar no Blender sem aplicar a escala
O braço direito ficou preto e o esquerdo não. Você espelhou a malha com Ctrl+M ou colocou escala -1 em X para criar o lado oposto e não aplicou a transformação antes de exportar. Escala negativa inverte a orientação de todos os triângulos daquele objeto — a malha continua certa na viewport do Blender, porque o Blender compensa, e chega invertida no Studio, porque o FBX carrega a escala como parte do objeto.
O que torna isso caro não é o conserto, é o ciclo. Você reimporta, olha, acha que melhorou porque a câmera está de outro ângulo, publica, e o bug reaparece só em algumas horas do dia — quando o sol muda de direção e passa a bater na face invertida. Aí você culpa o ciclo de dia e noite e vai mexer em ClockTime.
Saída: Object Mode, selecione tudo, Ctrl+A, All Transforms. Confirme no painel N que a escala voltou para 1,1,1 nos três eixos. Depois Edit Mode, A, Shift+N. Só então exporte. E valide sempre com o teste da checagem 2, que remove a direção da luz da equação — se você validar com o sol ligado, vai aprovar um mesh quebrado só porque o ângulo da câmera estava a seu favor.
O segundo erro caro do mesmo grupo: consertar tudo trocando Technology para Future e considerar encerrado. Future é o modo mais pesado da plataforma e dispositivos que não dão conta caem para um modo mais barato automaticamente. Ou seja: no seu PC a sombra ficou perfeita e no celular de boa parte dos jogadores continua o voxel de 4 studs, com a sombra em degraus exatamente como antes. Se o teu jogo é majoritariamente mobile, o conserto real é reduzir o número de sombras dinâmicas por personagem, não subir o modo de iluminação.
O que medir depois
Primeiro, teste com carga: coloque de 8 a 10 cópias do rig animado no mesmo lugar da cena, abra o Render Stats (Ctrl+Shift+F2) e olhe o tempo de frame. Sua meta é 16,6 ms por frame para 60 FPS e 33,3 ms para 30 FPS — se o tempo estoura quando os rigs entram em cena e volta ao normal quando você os deleta, o custo é de sombra dinâmica, não de script. Abra o MicroProfiler (Ctrl+F6) e confirme na etapa de render.
Segundo, teste no dispositivo real. Publique num place de teste privado e abra no celular, não no emulador de resolução do Studio — o downgrade automático de iluminação acontece por capacidade de GPU, e o emulador não reproduz isso. Se a sombra em degraus voltar no celular, você tem a resposta: precisa de menos luzes com Shadows, não de outro Technology.
Terceiro, transforme a checagem em rotina. Rode LightingDoctor.imprimir em cada rig novo antes de mandar para o jogo e anote quantos achados aparecem por personagem. Rig limpo sai com zero. Quando um rig novo sair com três ou quatro achados, você descobre na hora do import — e não depois que o jogador postou o clipe do braço preto no seu Discord.
Artefato pronto pra usar
--!strict
--[[
LightingDoctor
ModuleScript em ServerScriptService, nome exato: LightingDoctor
Uso no Command Bar do Studio (rig selecionado no Explorer):
local D = require(game.ServerScriptService.LightingDoctor)
D.imprimir(game:GetService("Selection"):Get()[1]) -- so relatorio
print(D.corrigir(game:GetService("Selection"):Get()[1])) -- aplica correcoes seguras
]]
local Lighting = game:GetService("Lighting")
export type Achado = {
caminho: string,
codigo: string,
detalhe: string,
corrigivel: boolean,
}
export type Opcoes = {
desligarSombraDeParteInvisivel: boolean?,
desligarSombraDeLuzNoRig: boolean?,
limiteLuzesPorRig: number?,
}
local LightingDoctor = {}
-- Materiais que se comportam mal em rig animado sob iluminacao dinamica.
local MATERIAIS_PROBLEMATICOS: { [Enum.Material]: string } = {
[Enum.Material.Neon] = "Neon ignora sombra recebida: a peca fica 'chapada' no meio da animacao.",
[Enum.Material.ForceField] = "ForceField nao recebe sombra e some sob luz forte.",
[Enum.Material.Glass] = "Glass recalcula refracao por frame e pisca em parte que se move rapido.",
}
local function novoAchado(inst: Instance?, codigo: string, detalhe: string, corrigivel: boolean): Achado
return {
caminho = if inst then inst:GetFullName() else "Lighting",
codigo = codigo,
detalhe = detalhe,
corrigivel = corrigivel,
}
end
-- Varre a cena e o rig e devolve a lista de achados, sem alterar nada.
function LightingDoctor.escanear(raiz: Instance, opcoes: Opcoes?): { Achado }
local o: Opcoes = opcoes or {}
local limiteLuzes: number = o.limiteLuzesPorRig or 1
local achados: { Achado } = {}
local tech = Lighting.Technology
if tech == Enum.Technology.Voxel or tech == Enum.Technology.Compatibility or tech == Enum.Technology.Legacy then
table.insert(achados, novoAchado(nil, "TECH_VOXEL", string.format(
"Lighting.Technology = %s. O grid voxel resolve luz em celulas de 4x4x4 studs: a sombra do rig anda em bloco e atrasada. Troque para ShadowMap ou Future no painel Properties (nao mude por script em runtime).",
tech.Name), false))
end
if tech == Enum.Technology.Future and Lighting.ShadowSoftness > 0.5 then
table.insert(achados, novoAchado(nil, "SHADOW_SOFT", string.format(
"ShadowSoftness = %.2f. Acima de 0.5 a borda da sombra 'ferve' enquanto o rig anima.",
Lighting.ShadowSoftness), false))
end
if Lighting.EnvironmentDiffuseScale <= 0.05 and Lighting.GlobalShadows then
table.insert(achados, novoAchado(nil, "AMBIENTE_ZERO", string.format(
"EnvironmentDiffuseScale = %.2f. Em cena interna o rig fica quase preto e parece bug de material.",
Lighting.EnvironmentDiffuseScale), false))
end
local luzesComSombra = 0
for _, d in ipairs(raiz:GetDescendants()) do
if d:IsA("BasePart") then
if d.Transparency >= 1 and d.CastShadow then
table.insert(achados, novoAchado(d, "SOMBRA_FANTASMA",
"Parte invisivel com CastShadow ligado: projeta sombra de caixa que nao acompanha a malha deformada.", true))
end
local aviso = MATERIAIS_PROBLEMATICOS[d.Material]
if aviso then
table.insert(achados, novoAchado(d, "MATERIAL", aviso, false))
end
if d:IsA("MeshPart") and d.DoubleSided then
table.insert(achados, novoAchado(d, "DOUBLE_SIDED",
"DoubleSided ligado: quase sempre curativo de normal invertida e dobra o custo de triangulo. Recalcule a normal no Blender (Ctrl+A em All Transforms, depois Shift+N) e desligue.", false))
end
elseif d:IsA("Light") then
if d.Shadows then
luzesComSombra += 1
table.insert(achados, novoAchado(d, "LUZ_COM_SOMBRA",
"Luz com Shadows ligado dentro de hierarquia animada: recalcula sombra a cada frame de animacao.", true))
end
if (d:IsA("PointLight") or d:IsA("SpotLight")) and d.Range > 30 then
table.insert(achados, novoAchado(d, "LUZ_RANGE", string.format(
"Range = %.0f. Range alto em luz que se move amplia o pop de atualizacao.", d.Range), false))
end
end
end
if luzesComSombra > limiteLuzes then
table.insert(achados, novoAchado(raiz, "EXCESSO_DE_SOMBRA", string.format(
"%d luzes com Shadows neste rig (limite sugerido: %d). Em mobile isso vira queda de frame antes de virar bug visual.",
luzesComSombra, limiteLuzes), true))
end
return achados
end
-- Aplica apenas o que e seguro automatizar. Nao toca em DoubleSided, Material nem Technology.
function LightingDoctor.corrigir(raiz: Instance, opcoes: Opcoes?): number
local o: Opcoes = opcoes or {}
local desligarInvisivel: boolean = if o.desligarSombraDeParteInvisivel == nil then true else o.desligarSombraDeParteInvisivel
local desligarLuz: boolean = if o.desligarSombraDeLuzNoRig == nil then true else o.desligarSombraDeLuzNoRig
local corrigidos = 0
for _, d in ipairs(raiz:GetDescendants()) do
if desligarInvisivel and d:IsA("BasePart") and d.Transparency >= 1 and d.CastShadow then
d.CastShadow = false
corrigidos += 1
elseif desligarLuz and d:IsA("Light") and d.Shadows then
d.Shadows = false
corrigidos += 1
end
end
return corrigidos
end
-- Relatorio legivel no Output.
function LightingDoctor.imprimir(raiz: Instance, opcoes: Opcoes?)
local achados = LightingDoctor.escanear(raiz, opcoes)
if #achados == 0 then
print(string.format("[LightingDoctor] %s: nada suspeito na luz. Se a mancha continua, o problema esta na malha (normal invertida ou custom split normals).", raiz.Name))
return
end
print(string.format("[LightingDoctor] %d achado(s) em %s", #achados, raiz:GetFullName()))
for i, a in ipairs(achados) do
local marca = if a.corrigivel then " (corrigivel por LightingDoctor.corrigir)" else ""
print(string.format(" %02d [%s] %s\n -> %s%s", i, a.codigo, a.caminho, a.detalhe, marca))
end
end
return LightingDoctor
{
"nome": "rig-animado-sem-bug-de-luz",
"blender_fbx_export": {
"aplicar_ctrl_a": ["Location", "Rotation", "Scale"],
"recalcular_normais_para_fora": true,
"remover_custom_split_normals": true,
"mesh_smooth_type": "Face",
"apply_modifiers": true,
"add_leaf_bones": false,
"bake_animation": true,
"path_mode": "Copy",
"embed_textures": true,
"scale": 1.0,
"max_influencias_por_vertice": 4
},
"studio_lighting_producao": {
"Technology": "Future",
"GlobalShadows": true,
"ShadowSoftness": 0.2,
"EnvironmentDiffuseScale": 0.5,
"EnvironmentSpecularScale": 0.5,
"Ambient": [0, 0, 0],
"OutdoorAmbient": [70, 70, 70]
},
"studio_lighting_teste_de_normal": {
"Ambient": [255, 255, 255],
"OutdoorAmbient": [255, 255, 255],
"Brightness": 0,
"GlobalShadows": false,
"material_da_parte": "SmoothPlastic",
"cor_da_parte": [255, 255, 255]
},
"limites_por_personagem": {
"luzes_com_shadows": 1,
"castshadow_em_parte_invisivel": false,
"double_sided": false,
"range_maximo_de_luz_animada": 30
}
}