Animação para Roblox

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.

por Gustavo Cantino7 min de leituraIntermediário

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

ModuleScript chamado LightingDoctor em ServerScriptService
lua
--!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
Preset de exportação FBX no Blender e valores de Lighting no Studio
json
{
  "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
  }
}
Neste artigo

Continue por aqui