Animação para Roblox

Texturizar um modelo feito no Studio sem quebrar a animação

Você encheu o modelo de Decal, a imagem esticou em cada peça e sumiu no dia que o rig virou skinned mesh. O caminho certo é exportar, abrir UV no Blender e voltar como MeshPart com SurfaceAppearance — com um script que audita e troca a skin inteira de uma vez.

por Gustavo Cantino11 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, 15/09/2026 Política Editorial

Placa tipográfica com a categoria Animação para Roblox em destaque, o nível intermediário abaixo, e um campo de barras verticais verde-limão sobre fundo escuro.

Textura em modelo feito dentro do Roblox tem exatamente três ferramentas — Decal, Texture e SurfaceAppearance — e a escolha entre elas decide se a textura continua grudada quando o modelo virar rig animado. Decal estica uma imagem até cobrir a face inteira da parte. Texture repete a mesma imagem a cada X studs, e SurfaceAppearance é o único que entrega PBR de verdade (cor, normal, rugosidade, metalicidade), só que ele exige um MeshPart — ou seja, exige passar o modelo por um unwrap de UV antes.

Se o seu modelo é um amontoado de Part montado no Studio e nunca vai ser animado, Texture resolve em cinco minutos. Se ele é um personagem, uma arma na mão do personagem ou qualquer coisa que um Animator vai mexer, o caminho é outro: exportar, abrir UV, voltar como MeshPart. É esse caminho que o resto do texto percorre.

180 Decals no rig e a textura esmagada na primeira animação

O cenário típico: personagem montado com umas 30 Parts no Studio, e você aplicou Decal nas faces visíveis de cada uma. Seis faces por parte, na conta cheia, dá 180 Decals. Isso é 180 instâncias a mais por personagem, replicadas do servidor pro cliente. Com 20 personagens num servidor, são 3.600 objetos existindo só para segurar imagem.

O problema visual aparece antes do problema de performance. Decal não respeita proporção: ele estica a imagem até preencher a face inteira. Uma imagem quadrada de 512x512 numa parte de 1x4x1 vira um padrão esmagado em 4:1. Como cada peça do corpo tem um tamanho diferente, o mesmo tecido aparece em cinco escalas diferentes no mesmo personagem, e isso fica óbvio quando a câmera chega perto.

Script em ServerScriptService
-- errado: seis instâncias por parte, e nenhuma delas respeita proporção
for _, face in Enum.NormalId:GetEnumItems() do
	local d = Instance.new("Decal")
	d.Face = face
	d.Texture = "rbxassetid://0"
	d.Parent = part
end

O prejuízo grande vem depois. No dia em que você levar esse personagem pro Blender pra fazer o skinning com bones — que é o que permite a deformação suave nas articulações em vez das juntas quebradas de Motor6D — as Parts deixam de existir. Vira um mesh só. Todo Decal vai junto e não tem exportação que salve: Decal não carrega UV, ele é uma projeção na face de uma caixa que você acabou de deletar.

E tem o detalhe que pega quem já mexeu com MeshPart: Decal dentro de um MeshPart não segue a UV do mesh, ele projeta na face da bounding box. Numa esfera ou num braço curvo o resultado é a imagem escorrendo pelas laterais.

Do modelo em Parts até o MeshPart com SurfaceAppearance

1. Escolha a ferramenta antes de subir imagem. Padrão repetido em superfície plana (piso, parede, madeira, grade) fica em Texture, com StudsPerTileU e StudsPerTileV iguais ao tamanho real do padrão em studs — assim a escala é a mesma em todas as peças, não importa o tamanho delas. Placa, pôster e cartaz ficam em Decal. Personagem, prop na mão e qualquer coisa que anima vai pro fluxo MeshPart.

2. Exporte o modelo. Selecione o Model no Explorer, File → Export Selection, salve como .obj. O Studio exporta a geometria nas posições em que ela está, então o encaixe das peças se mantém no Blender.

3. Abra a UV no Blender. Importe o .obj, entre em Edit Mode, selecione tudo com A e use U → Smart UV Project. Suba o Island Margin para algo entre 0.02 e 0.03: sem essa borda, o mipmap mistura os pixels de ilhas vizinhas e você vê costuras piscando quando a câmera se afasta. Depois, UV → Pack Islands para aproveitar o espaço.

4. Pinte ou bake num atlas único. Crie uma Image Texture nova de 1024x1024, e pinte no Texture Paint ou faça bake do material procedural (Bake Type: Diffuse, com Direct e Indirect desmarcados — você quer a cor, não a iluminação do Blender assada na textura). O objetivo é uma imagem só cobrindo o modelo inteiro.

5. Exporte em 1024x1024 e pare por aí. O Roblox reamostra qualquer imagem enviada acima de 1024x1024 para 1024x1024. Subir um 4096 não te dá mais detalhe, só te dá um upload mais lento e a falsa impressão de que a nitidez que você vê no Blender vai chegar no jogo. Um 1024x1024 RGBA ocupa 4.194.304 bytes descomprimidos na memória de vídeo, mais uns 33% de mipmaps — por isso um atlas único bate um monte de imagens pequenas espalhadas.

6. Volte pro Studio. Import 3D para trazer o .fbx ou .obj (ele cria o MeshPart), e Asset Manager → Images para subir a textura e pegar o assetId. Upload de imagem não custa Robux, mas passa por moderação antes de renderizar pros outros jogadores.

7. Monte o SurfaceAppearance. Insira um SurfaceAppearance como filho do MeshPart e preencha ColorMap com o id da imagem. NormalMap, RoughnessMap e MetalnessMap são opcionais, mas todos precisam compartilhar exatamente a mesma UV do ColorMap — se você reabrir o UV depois de fazer o normal, os três saem de sincronia. AlphaMode em Overlay mistura a imagem com a cor da parte (ótimo pra variação de time sem subir textura nova); Transparency usa o canal alfa da imagem.

8. Feche o ciclo com a animação. Como a UV mora no mesh, a textura acompanha a deformação: seja rig de Motor6D com uma peça por MeshPart, seja skinned mesh com bones. É esse o ganho real de ter saído do Decal — o rig pode deformar à vontade que o desenho continua no lugar.

Pra prop animado tem um bônus que só o Texture faz: rolar a imagem em vez de mover geometria. Esteira, correia, cachoeira e cano de fumaça saem de graça assim.

Script em ServerScriptService
local texture = script.Parent:FindFirstChildOfClass("Texture")
task.spawn(function()
	while texture do
		texture.OffsetStudsV -= 0.05
		task.wait(1 / 30)
	end
end)

O ModuleScript que audita o modelo e troca a skin inteira

O artefato abaixo é um ModuleScript chamado SkinApplier, para ficar em ServerScriptService. Ele faz duas coisas.

SkinApplier.Audit(model) percorre o modelo e imprime quantos Texture, quantos Decal, quantos MeshPart e quantas outras BaseParts existem ali dentro, além de avisar sobre SurfaceAppearance pendurado em coisa que não é MeshPart. É o número que você precisa ver antes e depois da migração: sair de 180 Decals para 8 MeshParts com SurfaceAppearance é uma métrica, não uma sensação.

SkinApplier.Apply(model, skin) recebe uma tabela que mapeia o Name de cada MeshPart para o conjunto de mapas PBR, cria o SurfaceAppearance se ele não existir, preenche ColorMap, NormalMap, RoughnessMap, MetalnessMap e AlphaMode, e devolve quantas peças foram aplicadas. Como roda em runtime, serve pra skin de time, skin de gamepass ou variação de NPC sem duplicar o modelo no ReplicatedStorage.

O ponto de atenção é o Name: a chave da tabela tem que bater com o Name do MeshPart dentro do modelo. Se o Import 3D renomeou suas peças para Mesh, Mesh1, Mesh2, renomeie no Explorer antes — ou o Apply devolve zero e você vai culpar o asset id.

O segundo artefato é o skins.json: mantenha a lista de ids fora da cabeça e versionada, porque asset id trocado por engano é o bug mais chato de achar (a textura simplesmente não aparece, sem erro no Output).

O erro caro: texturizar antes de decidir a topologia final

O erro que custa a tarde inteira não é escolher Decal em vez de SurfaceAppearance. É texturizar antes de decidir se aquele modelo vai terminar como peças separadas ou como um mesh só. Toda textura que você aplica em Part morre em duas operações comuns: Union e skinning. Ao unir partes, os Decals e Textures das peças originais não sobrevivem na UnionOperation. Ao exportar pro Blender e juntar tudo num mesh com bones, idem.

A regra prática: defina primeiro se o modelo vai animar. Se vai, ele termina como MeshPart, ponto. Só texturize depois que a malha estiver fechada e o UV feito.

Se você já está no buraco, a saída não é refazer o modelo — é rodar o Audit, anotar quais imagens você já subiu (elas continuam válidas, só precisam ser reposicionadas num atlas) e levar o .obj pro Blender montando a UV com as ilhas nas mesmas proporções das faces antigas. Você reaproveita a arte, não o posicionamento.

Dois erros menores que aparecem no mesmo dia: SurfaceAppearance colocado dentro de Part ou WedgePart aparece no Explorer e não renderiza nada (é por isso que o Audit avisa); e relevo que parece invertido, com os frisos afundando em vez de saltar, é canal verde do normal map na convenção errada — inverta o verde na exportação e compare lado a lado antes de subir de novo. Teste sempre com a Lighting Technology que o jogo usa de verdade: o que você ajustou no Future pode achatar bastante em outra tecnologia.

O que medir depois

Abra o Developer Console no jogo publicado, aba Memory, e olhe a linha de memória gráfica de textura antes e depois da migração — é lá que o atlas único mostra serviço, principalmente em celular. Rode o Audit nos seus três modelos mais usados e anote a contagem de instâncias: se um personagem ainda passa de 50 objetos de textura, tem atlas pra fazer.

Depois, cronometre a moderação. Suba uma imagem de teste, marque a hora e veja quanto tempo leva até ela renderizar numa sessão publicada — esse número define se você pode subir textura na véspera do update ou se precisa de folga. E guarde o skins.json no repositório junto com o .blend: no dia que a moderação recusar uma imagem, você troca um id em um arquivo em vez de caçar SurfaceAppearance no Explorer.

Artefato pronto pra usar

ModuleScript chamado SkinApplier em ServerScriptService
lua
--!strict
-- SkinApplier — ModuleScript em ServerScriptService
--
-- Uso, a partir de um Script em ServerScriptService:
--   local SkinApplier = require(game:GetService("ServerScriptService").SkinApplier)
--   SkinApplier.Audit(workspace.Boneco)
--   print(SkinApplier.Apply(workspace.Boneco, SkinApplier.Skins.GuardaReal), "peças texturizadas")

local SkinApplier = {}

-- Um conjunto de mapas PBR. Todos precisam compartilhar a MESMA UV do ColorMap.
export type MapSet = {
	ColorMap: string,
	NormalMap: string?,
	RoughnessMap: string?,
	MetalnessMap: string?,
	AlphaMode: Enum.AlphaMode?,
}

-- A chave é o Name do MeshPart dentro do modelo.
export type Skin = { [string]: MapSet }

SkinApplier.Skins = {
	GuardaReal = {
		Torso = {
			ColorMap = "rbxassetid://000000001",
			NormalMap = "rbxassetid://000000002",
			RoughnessMap = "rbxassetid://000000003",
		},
		Cabeca = {
			ColorMap = "rbxassetid://000000004",
			AlphaMode = Enum.AlphaMode.Transparency,
		},
	} :: Skin,
}

-- Cria o SurfaceAppearance se ainda não existir. SurfaceAppearance só renderiza dentro de MeshPart.
local function ensureSurfaceAppearance(mesh: MeshPart): SurfaceAppearance
	local existing = mesh:FindFirstChildOfClass("SurfaceAppearance")
	if existing then
		return existing :: SurfaceAppearance
	end
	local sa = Instance.new("SurfaceAppearance")
	sa.Name = "Skin"
	sa.Parent = mesh
	return sa
end

-- Conta o que existe de textura no modelo e avisa o que está no lugar errado.
function SkinApplier.Audit(model: Model): ()
	local textures, decals, meshParts, outrasPartes, orfaos = 0, 0, 0, 0, 0

	for _, inst in model:GetDescendants() do
		if inst:IsA("Texture") then
			-- Texture herda de Decal, então ele vem primeiro na checagem.
			textures += 1
		elseif inst:IsA("Decal") then
			decals += 1
		elseif inst:IsA("SurfaceAppearance") then
			local pai = inst.Parent
			if not (pai and pai:IsA("MeshPart")) then
				orfaos += 1
				warn(string.format("[SkinApplier] SurfaceAppearance fora de MeshPart em %s — não vai renderizar.", inst:GetFullName()))
			end
		elseif inst:IsA("MeshPart") then
			meshParts += 1
		elseif inst:IsA("BasePart") then
			outrasPartes += 1
		end
	end

	print(string.format(
		"[SkinApplier] %s — MeshParts: %d | outras BaseParts: %d | Textures: %d | Decals: %d | SurfaceAppearance órfãos: %d",
		model.Name, meshParts, outrasPartes, textures, decals, orfaos
	))
end

-- Aplica a skin. Devolve quantos MeshParts foram atingidos (zero = nome não bateu).
function SkinApplier.Apply(model: Model, skin: Skin): number
	local aplicados = 0

	for _, inst in model:GetDescendants() do
		if inst:IsA("MeshPart") then
			local set = skin[inst.Name]
			if set then
				local sa = ensureSurfaceAppearance(inst)
				-- SurfaceAppearance sobrepõe o TextureID do MeshPart; não precisa limpar nada.
				sa.ColorMap = set.ColorMap
				sa.NormalMap = set.NormalMap or ""
				sa.RoughnessMap = set.RoughnessMap or ""
				sa.MetalnessMap = set.MetalnessMap or ""
				sa.AlphaMode = set.AlphaMode or Enum.AlphaMode.Overlay
				aplicados += 1
			end
		end
	end

	if aplicados == 0 then
		warn(string.format("[SkinApplier] Nenhum MeshPart de %s bateu com as chaves da skin. Confira os Names no Explorer.", model.Name))
	end

	return aplicados
end

return SkinApplier
Arquivo skins.json no repositório do projeto (fonte dos ids colados no ModuleScript)
json
{
  "skinId": "guarda_real_v3",
  "atlas": "1024x1024",
  "origem": "guarda_real.blend / UV: Smart UV Project, island margin 0.03",
  "parts": {
    "Torso": {
      "ColorMap": "rbxassetid://000000001",
      "NormalMap": "rbxassetid://000000002",
      "RoughnessMap": "rbxassetid://000000003",
      "AlphaMode": "Overlay"
    },
    "Cabeca": {
      "ColorMap": "rbxassetid://000000004",
      "AlphaMode": "Transparency"
    },
    "BracoDireito": {
      "ColorMap": "rbxassetid://000000005",
      "NormalMap": "rbxassetid://000000002",
      "AlphaMode": "Overlay"
    }
  }
}
Neste artigo

Continue por aqui