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.
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

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.
-- 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
endO 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.
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
--!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{
"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"
}
}
}

