DEV Community

Richard Fu
Richard Fu

Posted on • Originally published at richardfu.net on

Re-skinning a 3D Character with AI Image Tools (Without Touching Blender)

TL;DR: I bought a well-made chibi farmer model and wanted to reuse it as a zookeeper character — same mesh, same rig, same animations. Meshy.ai’s Retexture didn’t work for me, and naive cropping of the UV atlas pulled in neighbor pieces. The fix turned out to be embarrassingly simple: connected-component labeling to extract one UV island at a time, send each piece to ChatGPT with a tight prompt, then composite the AI result back with an intersection-of-silhouettes mask. I packaged the workflow as a Claude Code skill — furic/texture-atlas-roundtrip — and ended up with a wildlife keeper that took a couple of evenings instead of a couple of weeks.


The problem

I’m a developer with no artist skills. For my causal game prototype, I needed a wildlife keeper character. I’d already paid for a beautifully animated chibi farmer (Suriyun’s Farmer SD pack), and the rig + animations were exactly what I wanted. The catch: the outfit was a plaid shirt + denim jeans + straw hat with a red band. Pure farmer.

What I needed:

  • A khaki keeper shirt with chest pockets
  • Khaki cargo pants/shorts
  • A pith helmet instead of a straw hat
  • Brown leather boots

What I wanted to not do:

  • Touch the FBX, the rig, or any animation
  • Open Blender to re-UV anything
  • Hand-paint a 2048×2048 texture from scratch

The character has one big packed texture atlas — Famer.png — with the shirt front, shirt back, sleeves, pants, belt, hat, leather accessories, and a few small bits all unwrapped onto a single image. That single PNG is what I needed to modify.


What didn’t work

Meshy.ai’s retexture function. I uploaded the FBX and tried both “text input” and “image input” prompts. The results were unrecognizable — colors smeared in the wrong places, details bleeding across UV seams. It felt like the model wasn’t understanding the UV mapping of the imported asset. Maybe there’s a config I missed, but I didn’t dig in: a one-click solution will probably exist in 12 months, but right now it doesn’t.

Asking ChatGPT to redraw the whole atlas. DALL-E doesn’t preserve UV island shapes pixel-precisely. Even with a strict “keep dimensions, keep silhouette” prompt, you get back a texture that looks roughly right but seams shift, the hat brim rotates 5°, the collar moves. UV-bound textures are unforgiving — even a few-pixel shift causes visible artifacts on the 3D mesh.

Cropping a single piece out as a rectangle. The atlas packs UV islands tightly to save memory, so the shirt-front bbox always includes slivers of the belt above and the suspender stripes that are technically in the same connected pixel region. Sending that to AI and pasting back contaminated the neighbor pieces.

So the path forward needed to be: edit one piece at a time, keep its silhouette pixel-perfect, and protect everything else from collateral damage.


The solution: split → edit → composite

The technique has three parts, and only the splitting bit is non-obvious:

  1. Split the atlas into per-UV-island PNGs. Each piece is a 1024×1024 canvas with one island centered and everything else (including neighbor islands that share its bbox) masked to pure black.
  2. Edit each piece independently in any AI image tool. Because the background is solid black, the AI has a clean canvas and won’t draw beyond the island silhouette.
  3. Composite the AI’s result back into the atlas at the exact original pixel coordinates, using a mask that protects neighbor islands from accidental overwrites.

Why naive cropping fails — and what to do instead

The atlas has roughly nine clearly visible UV islands, but they sit close together in the 2D image:

+-------------------------------+
|                               |
|   [BELT — wide horizontal]    |
|                               |
|  [SHIRT FRONT]   [SHIRT BACK] |
|                               |
|                  [SLEEVES][..]|
|                               |
|  [PANTS]        [STRAW HAT]   |
|                               |
+-------------------------------+

If I rectangle-crop “shirt front”, I’m guaranteed to also catch a slice of the belt above it or the leather puffs on its right edge. Send that to AI, and the AI either repaints those slivers (corrupting the belt) or, more often, fills them with black — and “fills with black” is exactly what destroys the belt when I paste back.

The fix is to extract the connected component rather than a rectangle. scipy.ndimage.label walks the image, assigns each contiguous non-black region its own integer ID, and gives you a per-pixel labels array. Then I crop the bbox AND zero every pixel whose label isn’t the one I want:

import numpy as np
from PIL import Image
from scipy import ndimage

img = np.array(Image.open("atlas.png").convert("RGBA"))
non_black = img[..., :3].sum(-1) > 30   # tolerate near-black gradients

labels, n = ndimage.label(non_black)
sizes = ndimage.sum(non_black, labels, range(1, n + 1))
slices = ndimage.find_objects(labels)

# Pick the island we want (e.g. by bbox heuristic or label_id)
target_label = 5
y0, y1 = slices[target_label - 1][0].start, slices[target_label - 1][0].stop
x0, x1 = slices[target_label - 1][1].start, slices[target_label - 1][1].stop

# Extract just this island — neighbors masked to black
piece = img[y0:y1, x0:x1].copy()
mask = labels[y0:y1, x0:x1] == target_label
piece[~mask] = [0, 0, 0, 255]

Now I have a piece I can drop onto a 1024×1024 black canvas, save out as shirt_front.png, and hand to any AI tool with a clean conscience.

I also save a manifest.json recording each piece’s original src_bbox, canvas_offset, label_id, and (we’ll come back to this) flip_y. The manifest is what makes the round trip work — without it, pasting back at the exact pixel position is guesswork.

The intersection-of-silhouettes paste-back

When the AI returns its result, the obvious composite is “paste the whole AI image into the original bbox.” But there are two failure modes hiding in there:

  • Failure 1: AI’s pure-black background covers neighbor islands that happen to fall inside the bbox. Belt fragment that lived in the corner of the shirt-front bbox? Now black.
  • Failure 2: AI drew slightly outside the original silhouette (e.g. extended the shirt by 5px on the left). Those extra pixels land on parts of the texture that map to invisible mesh — but worse, they can land on a different UV island in the same bbox.

The fix is the intersection of two silhouettes:

# AI silhouette: where the AI actually drew content
ai_silhouette = ai_result.sum(-1) > 30

# Original silhouette: where the target UV island lives in the atlas
src_labels, _ = ndimage.label(original_atlas.sum(-1) > 30)
orig_silhouette = src_labels[y0:y1, x0:x1] == target_label

# Paste only where both agree
mask = ai_silhouette & orig_silhouette
target[y0:y1, x0:x1][mask] = ai_resized[mask]

Original-silhouette-only would let AI black overwrite neighbors. AI-silhouette-only would let AI bleed onto invisible mesh. The intersection covers both directions in one line.

I add a tiny erosion + Gaussian blur on the mask to avoid hard edges where AI’s stroke meets the existing texture. That single trick is what makes the round trip clean enough to ship.


The Y-flip gotcha

I sent shirt_front.png to ChatGPT with a careful prompt — “khaki shirt, two chest pockets, keep dimensions and silhouette” — and got back an excellent result. Pasted it back, opened Unity, and the pockets were on the keeper’s waist. Upside down.

Took me a minute to figure out why: the chibi character’s UV unwrap stores the shirt vertically flipped relative to mesh space. The V-notch at the bottom of the texture? That’s the collar in 3D. When the AI looked at the piece, it saw a normal shirt right-way-up (because the collar happens to point down in the texture) and drew pockets in the upper half. Those pockets, mapped back to mesh, landed on the lower torso.

The fix has two flavors:

Flavor A — don’t pre-flip; flip the AI result during composite.
Pockets will still be in the wrong half of the canvas, but the second flip puts them on the chest. Works for symmetric content.

Flavor B — pre-flip the input so AI works in natural orientation.
Send a Y-flipped piece to AI, AI draws on a “right-side-up” shirt, then composite flips the result back. Two flips total. Better for asymmetric content (name tags, logos).

I went with Flavor B and added a flip_y: true field to the manifest entry for each Y-flipped piece. The composite step honors it automatically:

if effective_flip_y:
    edited_pil = edited_pil.transpose(Image.FLIP_TOP_BOTTOM)

Discovery flow: composite once, see that pockets are upside-down, set flip_y: true in the manifest, re-composite. No re-prompting needed.


The workflow in practice

Here’s the actual loop I ran for the keeper conversion:

SKILL=~/.claude/skills/texture-atlas-roundtrip

# 1. Split the farmer atlas into per-UV-island PNGs
python3 $SKILL/split.py Famer.png ./pieces/

# 2. Rename pieces in manifest.json (island_2 → shirt_front, etc.)
#    Mark Y-flipped pieces: "flip_y": true

# 3. (For Y-flipped pieces) pre-flip so AI sees natural orientation
python3 $SKILL/flip_piece.py shirt_front.png  # → shirt_front_flipped.png

# 4. Send each piece to ChatGPT with a prompt like:
#    "Redraw this UV texture as a khaki keeper shirt with two chest pockets,
#     keep 1024×1024 dimensions, pure black background, exact silhouette..."
#    Save the result back to the same filename.

# 5. Composite each edited piece into the keeper atlas
python3 $SKILL/composite.py M07_Keeper.png shirt_front shirt_front_edited.png ./pieces/manifest.json

For the hat I used a slightly different sub-flow: extract the hat region with the connected-component mask, send to ChatGPT with a pith-helmet reference photo and a “top-down circular UV” prompt, composite back. Since the existing straw hat already had the right round silhouette for a top-down dome+brim view, AI just had to repaint the texture inside that silhouette — no UV alignment risk.

Pants had one extra wrinkle: the “pants” UV island is actually pants AND boots in one piece, with a visible curve dividing them. After the first AI pass turned the whole island into khaki, the boots came out as khaki socks. Second pass with a “modify only below the curve” prompt added brown leather boots without disturbing the pants area.


Results

Before:

After:

The model, rig, animations, and FBX are completely untouched. The keeper texture is a single recolored + AI-edited variant of Famer.png, drop-in compatible with the original material.

For comparison, here’s Meshy.ai’s attempt — which is what pushed me to build this in the first place:


Get the skill

I packaged the workflow as a Claude Code skill: furic/texture-atlas-roundtrip.

git clone https://github.com/furic/texture-atlas-roundtrip ~/.claude/skills/texture-atlas-roundtrip
pip install numpy Pillow scipy

The repo includes:

  • SKILL.md — when to use, common mistakes, paste-back masking logic
  • split.py — atlas → per-island PNGs + manifest
  • composite.py — paste-back with intersection mask + Y-flip support
  • flip_piece.py — vertical flip helper for Y-flipped UVs
  • examples/ — optional /split-texture and /merge-texture slash command shortcuts

Roughly 200 lines of Python total. The whole thing exists because connected-component labeling solves an entire class of UV editing problems if you let it.


What I’d want next

A model-aware AI image tool that understands the UV → mesh mapping would make all of this obsolete. Meshy.ai’s retexture is the closest thing today and it didn’t work for my chibi character; presumably it’ll be solved in the next round. Until then, splitting an atlas and round-tripping pieces through ChatGPT is the most reliable workflow I’ve found, and it’s good enough that I’m planning to use it for the next four characters in this series.

If you’ve been blocked on reskinning a 3D asset because you’re not an artist, give the skill a try and tell me where it falls down — issues and PRs welcome at the repo.

The post Re-skinning a 3D Character with AI Image Tools (Without Touching Blender) appeared first on Richard Fu.

Top comments (0)