Roblox Animation

Roblox GFX: Pose the Rig in Studio Before Blender

Your avatar shows up white in Blender, the pose snaps back to T every time you rotate an arm, and the render looks like wet plastic. Assemble and pose the rig inside Studio through the Motor6D C0s, export it already frozen, and leave Blender only the light, the camera and the noise.

by Gustavo Cantino10 min readIntermediate

AI-assisted draft, reviewed, tested and edited by a human before publishing. See our Editorial Policy. · Reviewed by Gustavo Cantino, 9/20/26 Editorial Policy

Typographic plate with the Roblox Animation category in large type, the intermediate level below it, and a field of vertical lime green bars on a dark background.

The GFX isn't bad in Blender. It's bad at 150 pixels

You want help making GFX — that rendered-avatar art that becomes your game's icon and thumbnail — and you're stuck on one of three things: the avatar shows up in Blender white, no shirt, no accessories; the pose looks like a mannequin frozen in a T; or the render comes out grainy, like wet plastic. All three have a fix, and none of them is "buy a better GPU." The order that actually works is to assemble the rig already posed inside Studio, export it posed, and leave Blender only what Blender is good at: light, camera and noise.

Before you touch anything, understand where this file ends up. The experience icon is uploaded at 512x512, but in the games list inside the app it renders at around 150 pixels per side — less than 10% of the area you're staring at on your monitor. The thumbnail goes up at 1920x1080 and gets shown in a card far smaller than that on the home page. So that sneaker texture detail that cost you 40 minutes does not exist for the player. What exists is silhouette, contrast and face.

The damage is measurable. Commissioned GFX costs money, and GFX you make yourself costs 4 to 8 hours the first time. If after that your icon's click rate doesn't move, you didn't just lose the hours: you lost the launch window, which is when the discovery algorithm hands you impressions for free. It's worth more to make a GFX that's legible at 150px in 90 minutes and measure it than to spend 6 hours on something nobody can decipher at actual size.

Eight steps, in the order that saves rework

1. Build the avatar inside Studio, don't download it from the website. The Roblox avatar page gives you a 2D render, not a mesh. What you want is the 3D character with shirt, pants, accessories and layered clothing already applied. On the server, Players:CreateHumanoidModelFromUserId returns exactly that: a finished Model, assembled with the same pipeline the game uses. That kills the most common "the avatar arrived white in Blender" scenario — it arrived white because you exported a Dummy from the Avatar tab, which has no clothing at all.

2. Pose through the Motor6Ds, never through the part's CFrame. This is the mistake that makes people quit GFX. You rotate the arm in the Explorer, it snaps back, you rotate it again, it snaps back again:

WRONG excerpt — Studio Command Bar
local rig = workspace.RigGFX
-- the arm rotates for 1 frame and the Motor6D yanks it back into place
rig.RightUpperArm.CFrame = rig.RightUpperArm.CFrame * CFrame.Angles(0, 0, math.rad(45))

In R15 every limb is attached to its parent by a Motor6D (RightShoulder, LeftElbow, Waist, Neck...). As long as the Motor6D exists, it owns the child part's CFrame every frame. What you need to rotate is the Motor6D's C0, in degrees converted with math.rad. Then the pose sticks.

3. Freeze the pose before exporting. After writing the C0s, wait one Heartbeat so the physics engine recomputes the world positions, and only then set Anchored = true on every BasePart. If you anchor first, the Motor6D can't move anything and the rig exports in T-pose even with the "right" code. Order matters: pose, wait a frame, anchor.

4. Bring the model into Edit mode. The rig is born during Run (F8), and everything that happens in Run disappears when you hit Stop. With the game still running, select the model in the Explorer, Ctrl+C, Stop, Ctrl+V. Now it lives in your real file, posed and anchored, and you can save that .rbxl as the base for your next GFX.

5. Export and import with an eye on scale. With the model selected, File → Export Selection → .obj. Studio spits out the .obj, the .mtl and an atlas PNG with all the textures. In Blender, import it and check the rig height in the N panel: if the avatar is under 1 unit tall, lights measured in Watts will look dead and you'll think you messed up the lighting when what you messed up was the scale. Scale the rig to something around 2 units and work with distances in meters.

6. Fix the materials in 2 clicks per texture. The .obj importer hands you the material with Linear interpolation, which smears the face and the hand-drawn textures — and every Roblox face is small pixel art stretched out. On the Image Texture node, switch Interpolation to Closest. For hair, glasses and any accessory with a transparent background, plug the image's Alpha into the Principled BSDF's Alpha and set Blend Mode to Alpha Clip with threshold 0.5. Without that, hair turns into a black square — the second biggest reason GFX gets abandoned halfway through.

7. Three lights and a long lens. Key light: Area, 45 degrees in front of and above the face, about 3 units out, with a big light size (1 to 2 units) so the shadow stays soft. Fill: opposite side, a quarter of the key's power, just so the shadow doesn't become a black hole. Rim: behind and slightly above, high power and a saturated color — the rim is what draws the silhouette, and it's what makes the character pop off the background at 150 pixels. On the camera, use 85mm. A 24mm or 35mm lens warps the face and screams amateur instantly.

8. Render with alpha and composite afterward. In Render Properties, Cycles, Max Samples between 128 and 192, Noise Threshold 0.02 and the denoiser on (OptiX if you have NVIDIA). Under Film, check Transparent. Save as PNG with RGBA. The background — gradient, scenery, blur — you build on top in Photopea or Photoshop, on a separate layer. Doubling samples from 192 to 4096 multiplies render time by more than 20 and doesn't change a visible pixel once the image is scaled down to card size.

The script that builds and poses the rig before export

The artifact below is a Script for ServerScriptService. It handles steps 1 through 3 on its own: it fetches the avatar by UserId, applies the angle table to the Motor6Ds by name, waits a frame and anchors everything.

Swap USER_ID for the ID of the profile you want to render (the number in the profile URL). The POSE table is in degrees, and each entry is the exact R15 Motor6D name — touch RightShoulder and LeftShoulder first, they're the ones that give the pose attitude. Values between -90 and 90 cover almost everything; past that the shoulder clips through the torso.

After pasting, hit F8 (Run, not Play). The Output prints how many joints were posed — if it prints zero, the avatar was R6 and the joint names are different. With the rig in the Workspace, Ctrl+C, Stop, Ctrl+V, then go to Export Selection.

The second artifact is the Blender value sheet: camera, the three lights and the render settings. It's not a file to import — these are the numbers you type into the panels, collected in one place so you're not hunting through a tutorial in the middle of the job.

The expensive mistake: judging the GFX full screen

The mistake that costs the most isn't technical. It's approving the GFX while looking at it at 1920x1080 on your 27-inch monitor. Full screen, any render with a denoiser looks good. The player is never going to see that image.

The test that fixes it takes 30 seconds: open the final PNG, scale it down to 150 pixels wide, step back half a meter from the screen and count to three. If in three seconds you can't say who the character is and what the action is, the GFX failed — and it failed for one of these reasons, in this order of frequency: the character is too small in the frame, the background has contrast too close to the character's, or there's no rim light separating the two.

The fix is always the same and it's fast: move the camera in until the head takes up close to 40% of the image height, darken or blur the background, and raise the rim's power. None of that requires re-rendering at full quality — test at 25% resolution and 32 samples, which comes out in seconds, and only render the final version once the 150px test passes.

The second expensive mistake lives in step 8: rendering without Film Transparent. You close Blender, the client (or you, two days later) asks for a different background, and you can't — the background is baked into the pixel along with the shadow and the reflection. Redoing it is half an hour. Checking one checkbox is half a second.

What to measure after you swap the icon

GFX doesn't get judged by "it looks nice." It gets judged by click rate. In the Creator Dashboard, under the Analytics tab, you have impressions and plays for your experience — the ratio between the two is the number the icon moves. Write down the value for the 7 days before the swap, publish the new icon, and compare against the 7 days after, without touching anything else in that same period.

Change one variable at a time. If you upload a new icon, a new thumbnail and change the game's name on the same afternoon, you'll never know which of the three moved the needle — and you'll repeat the other two for free on your next launch. Pose and framing first, background second, text last.

And save the .blend with the lights and the camera already positioned. The second GFX, reusing the scene and swapping only the exported rig, comes out in 20 or 30 minutes. That's what separates someone who makes one GFX from someone who can test four different thumbnails in a week.

Ready-to-use artifact

Script in ServerScriptService
lua
--!strict
--[[
	GFX Rig Builder
	Where it lives: a Script (not a LocalScript) inside ServerScriptService.
	How to use:
	  1. Replace USER_ID with the profile ID (the number in the Roblox profile URL).
	  2. Adjust POSE (values in DEGREES).
	  3. Press F8 (Run), NOT Play.
	  4. With Run on: select RigGFX in the Explorer, Ctrl+C, Stop, Ctrl+V.
	  5. File > Export Selection > .obj
]]

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

-- ID of the owner of the avatar you're going to render
local USER_ID: number = 156 -- builderman, replace with yours

type Rot = { x: number, y: number, z: number }
type Pose = { [string]: Rot }

-- Exact names of the R15 Motor6Ds. Values in degrees.
-- Start by tweaking RightShoulder / LeftShoulder: they're the ones that give attitude.
local POSE: Pose = {
	Waist          = { x = 0,   y = -12, z = 0 },
	Neck           = { x = 6,   y = 14,  z = 0 },
	RightShoulder  = { x = 0,   y = 0,   z = 65 },
	LeftShoulder   = { x = 0,   y = 0,   z = -38 },
	RightElbow     = { x = -22, y = 0,   z = 0 },
	LeftElbow      = { x = -55, y = 0,   z = 0 },
	RightHip       = { x = -8,  y = 0,   z = 0 },
	LeftHip        = { x = 12,  y = 0,   z = 0 },
	RightKnee      = { x = -14, y = 0,   z = 0 },
	LeftKnee       = { x = -6,  y = 0,   z = 0 },
}

-- Rotates the C0 of every Motor6D whose name appears in the POSE table.
-- Rotating the part's CFrame directly does NOT work: the Motor6D rewrites it on the next frame.
local function applyPose(model: Model, pose: Pose): number
	local applied = 0
	for _, inst in model:GetDescendants() do
		if inst:IsA("Motor6D") then
			local rot = pose[inst.Name]
			if rot then
				inst.C0 = inst.C0 * CFrame.Angles(math.rad(rot.x), math.rad(rot.y), math.rad(rot.z))
				applied += 1
			end
		end
	end
	return applied
end

-- Freezes the rig in place. ONLY after the pose has been applied and 1 physics frame has passed.
local function freeze(model: Model)
	for _, inst in model:GetDescendants() do
		if inst:IsA("BasePart") then
			inst.Anchored = true
			inst.CanCollide = false
		end
	end
end

local ok, result = pcall(function()
	-- Assembles the full avatar: shirt, pants, accessories and layered clothing.
	return Players:CreateHumanoidModelFromUserId(USER_ID)
end)

if not ok or typeof(result) ~= "Instance" then
	warn("[GFX] Failed to assemble the avatar for UserId " .. tostring(USER_ID) .. " -> " .. tostring(result))
	return
end

local rig = result :: Model
rig.Name = "RigGFX"
rig.Parent = workspace
rig:PivotTo(CFrame.new(0, 5, 0))

-- Prevents the Humanoid's state machine from pushing the character back into Idle.
local humanoid = rig:FindFirstChildOfClass("Humanoid")
if humanoid then
	humanoid.AutoRotate = false
	humanoid:ChangeState(Enum.HumanoidStateType.Physics)
	local animator = humanoid:FindFirstChildOfClass("Animator")
	if animator then
		animator:Destroy() -- no default animation overriding the pose
	end
end

local applied = applyPose(rig, POSE)
RunService.Heartbeat:Wait() -- lets the Motor6Ds recompute the world before anchoring
freeze(rig)

if applied == 0 then
	warn("[GFX] No joints posed. The avatar is probably R6 and uses different Motor6D names.")
else
	print(string.format("[GFX] RigGFX ready in the Workspace with %d posed joints. Ctrl+C now, Stop, Ctrl+V.", applied))
end
Value sheet to type into Blender (Render Properties, Light Data and Image Texture)
json
{
  "rig_scale": { "target_height_units": 2.0, "note": "if lights in Watts look weak, the problem is scale, not power" },
  "camera": { "focal_length_mm": 85, "distance_units": 6.0, "target_height": "rig eye line", "head_takes_up_of_height": 0.4 },
  "lights": [
    { "name": "key", "type": "AREA", "power_w": 900, "size": 1.5, "horizontal_angle_degrees": 45, "height_units": 4.0, "color": "#FFFFFF" },
    { "name": "fill", "type": "AREA", "power_w": 220, "size": 2.0, "horizontal_angle_degrees": -50, "height_units": 2.0, "color": "#CFE2FF" },
    { "name": "rim", "type": "AREA", "power_w": 1400, "size": 0.8, "horizontal_angle_degrees": 165, "height_units": 3.5, "color": "#FF6A3D" }
  ],
  "materials": { "image_texture_interpolation": "Closest", "blend_mode": "Alpha Clip", "alpha_threshold": 0.5, "skin_roughness": 0.55, "fabric_roughness": 0.75 },
  "render": { "engine": "CYCLES", "max_samples": 192, "noise_threshold": 0.02, "denoiser": "OPTIX", "film_transparent": true, "view_transform": "AgX or Filmic" },
  "output": { "icon_px": [512, 512], "thumbnail_px": [1920, 1080], "format": "PNG", "color": "RGBA", "quick_test": { "resolution_percent": 25, "max_samples": 32 } },
  "final_checklist": ["scale down to 150px and identify the character in 3s", "background with contrast different from the character", "rim light visible on the silhouette"]
}
In this article

Keep reading