scenario-labs / scenario-blender-uv-baking
Use when UV unwrapping, placing seams, straightening or packing UVs, setting texel density, laying out UDIMs or mirrored UVs, or baking normal, AO, curvature, ID or displacement maps from a sculpt or high poly onto a low poly or retopo in Blender, including Multires bakes. Also when a bake has black or inverted patches, flat areas, cracks, seams, wavy lines or AO ghosts, bpy.ops.object.bake returns CANCELED, or uv.unwrap silently fails.
UV unwrapping and baking
Expert-level UVs put seams where the texturing method can afford them, set texel density before texture size, and pack with pixel-true padding. Expert bakes measure their projection distances instead of guessing, and are judged in pixels and under a raking light, never eyeballed once. Every stage below has a measurable gate that [`scripts/bx_uvbake.py`](scripts/bx_uvbake.py) computes. If a sibling skill named here is missing from your available skills, ask the user to install it (npx skills add scenario-labs/skills --skill <name>); unattended, proceed from tool schemas and flag the gap.
REQUIRED BACKGROUND: scenario-blender-expert (execution channel, review loop, 5.2 API changes).
Stance (the expert delta)
- Seams follow the texturing method. Kaspar (2021, 2D painting): as few as possible, hidden under hair, clothes and inner limbs. Blender Studio guide (2024, same author): more seams rather than fewer when you paint in 3D or bake, because stretch costs more. Game bakes add a hard rule: every hard edge is a seam, or the normal map cracks (Lampel, Gambrell, On Mars 3D, Polycount).
- Only closed shells and long tubes need extra cuts. Morren: anything closed on itself needs one cut. Gambrell: cut every taped-paper ring once, hidden. But an island with holes (a face with eye holes, a washer) flattens fine.
cut_to_diskstest-unwraps it and cuts only if it stretches [added; cutting everything folded the sheep's big islands]. - Unwrap method is a measurement, and 5.2 defaults to CONFORMAL. Always pass
method=. On Suzanne, Minimum Stretch gives the most even density (spread 1.38), Angle Based the best angles, and Conformal fails by area (5.89). On flat hard-surface islands all three converge.unwrap(method="BEST")picks per island and rejects islands that fold over themselves. - Density first, texture size second (Huge Menace). Targets: FPS 10.24 px/cm, weapon 20.48, third person 5.12, top-down 2.56. Priority follows the camera: face, hands and front up; soles, inner mouth and back down (SpeedChar, Kaspar). Match texture sets by scaling the denser set down, never the other up (Gambrell).
- Straighten, then equalize, then pack without tilting (Lampel): Follow Active Quads from a truly rectangular quad, pin, re-unwrap the rest, pack with CARDINAL. Padding is a pixel number, res/128 (16 px at 2K), passed as FRACTION margin = gap/(2*res).
- Measure the projection, never guess extrusion, and do not trust one global value. Ryan King's 0.01 then 0.02 was scene-specific. SpeedChar swept six values by hand, and the value that fixed the face broke the armpit ("when we fix something somewhere, some other problems occur", E6QMhGuhyoc [00:26:23]). Rays start on the low, and
max_ray_distancecounts from the extruded start. The adaptive bake keeps, per texel, the smallest extrusion that hits a front face. On the sheep it cut inverted texels from 7.5% to 1.2% [verified]. - Unique UVs wherever baked lighting differs (SpeedChar: an AO ghost means shared texels). Stacked or mirrored copies move exactly one tile out of 0-1 before the bake (Polycount), because inside 0-1 the last island baked wins.
- A bake is judged in pixels and under light. Blue < 0.5 means rays hit the far side. Exactly flat means rays missed. All-white AO means the distance is too small. Inverted pixels that do not move when extrusion changes point to a geometry problem, not a setting [verified on the sheep]. Multires bakes cast no rays: on the same surface they gave 0.09% inverted texels.
Establish first
| Input | Changes | Default when the brief is silent |
|---|---|---|
| Target | seams, symmetry, UDIM vs sets, triangulation | game engine, one 0-1 set per material |
| Texturing method | seam count and visibility | 3D painting/Substance: seams may show at the back |
| Camera | density and priorities | third person 512 px/m, closest view defines tiles |
| Texture budget | resolution, sets, UDIM count | 2048 per set; texture_size_for decides |
| Deformation | seams off joints, tangent-space normals | character: tangent space, unique UVs |
| High source | Selected to Active vs Multires | separate sculpt: Selected to Active |
| Engine convention | normal green channel | OpenGL (Blender, Unity, glTF); directx=True for Unreal |
Workflow
- Pre-flight. Apply scale (On Mars 3D: it "fixes the vast majority" of problems). Clear non-manifold geometry, doubles and back faces (SpeedChar). Decide on Mirror and Solidify modifiers. If UVs exist, compare
island_topologywithuv_qaislands: seam flags in files are often leftover retopology marks, so runseams_from_uv_islands(Lampel) before re-unwrapping. GATE: scale 1,bx_auditclean, topology table read. - Seams. Hard surface:
seams_from_sharp(obj, 30, mark_sharp=True). Organic: build part labels (head, ears, limbs, torso front and back, soles, inner mouth and eyelids per the Kaspar and Morren seam maps in [`references/expert-notes.md`](references/expert-notes.md)), thenseams_from_groups(obj, labels). Specific cuts:seam_path(obj, v_a, v_b, view_dirs). Thencut_to_disks(obj, view_dirs=[camera]). GATE:must_cutempty; a checker render shows no seams across hero areas that the brief cannot afford. - Unwrap.
unwrap(obj, method="BEST"), or the method the island type calls for. GATE:unsolved_faces0 (the operator returns FINISHED even when it fails),folding_islandsempty (elseseams_from_groups(..., faces=folding_faces)and unwrap again), angle p95 at most 15 to 20 deg. - Straighten grid-like islands (hard surface, garments, strands):
straighten(obj)before any scaling. GATE: straight stripes in the checker. - Texel density.
normalize_texel_density(obj, target, res, weights), thentexture_size_for(obj, target). GATE: island spread within 10-20%, or the intended priority ratios. - Pack.
pack(obj, tex_res=res), withscale=Falsefor a strict density andallow_any=Truefor organic islands. UDIM: pack each region, thenmove_to_tile. GATE:uv_qa: 0 overlaps, gap at least res/128, border at least gap/2, coverage at least 0.70 or an explained exception. - Look.
uv_layout_image,checker_review,distortion_review(metric="angle"). Judge with [`references/critique.md`](references/critique.md). - Bake prep.
prepare_low(low, triangulate=game, sharp_from_uv_islands=hard_surface). Check the silhouette against the high (bx_audit.audit(low, high=...)). Explode interpenetrating or moving parts (explode, one axis). Runmeasure_projection:miss_pctunder 2 andwrong_part_pctunder 1, else use a cage, explode, or bake per pair. - Bake.
bake_high_to_low(low, highs, out, res=512)first, then the final resolution. It uses adaptive extrusion by default and hides every other object from render. Multires:bake_multires. Extras:bake_id,curvature_from_normal. GATE:sanity[...]["problems"]empty (inverted texels under 2%, notes from 0.2%), 16-bit PNG. If inverted texels remain, runmap_problem_reviewto see where. Ifsweep_projectionshows they do not move with extrusion, fix the geometry (fit, holes, thin parts), not the numbers. - Review and hook up.
bake_review(low, highs, paths, out): the low with maps must read like the high under the raking light. Thenhookup_maps. Report measured numbers and what was not verified.
Numbers
| Value | Relative to | Source |
|---|---|---|
| 10.24 / 20.48 / 5.12 / 2.56 px/cm | FPS world / FPS weapon / third person / top-down | Huge Menace [00:12:28] |
| 10-20% density mismatch invisible | between islands or sets | Gambrell [01:36:16] |
| coverage above 70%, 75% studio minimum | one 0-1 tile | Gambrell [01:31:56], Huge Menace [00:16:27] |
| island gap res/128 (8/16/32 px at 1K/2K/4K) | texture resolution | Lampel [00:38:01], On Mars 3D [00:11:18] |
| FRACTION margin m: gap 2m, border m | Blender pack | verified |
| bake margin res/128, 16 px at 2K | texture resolution | On Mars 3D, digest |
| cylinder sides: 6 min, 8 recommended | low poly silhouette | On Mars 3D [00:03:48] |
| samples: 16 for normals, 128 for AO, 1 only while iterating | antialiasing and noise | Gambrell/Marmoset 16, SpeedChar 4x4, verified |
| test bake 512, final 2048 | iteration loop | SpeedChar [00:59:09] |
| AO distance about 0.2 x bbox diagonal (default 10 units) | asset size | [added], verified effect |
| angle error p95 at most 15 deg organic, 3 deg flat | per corner | [added] from 5.2.1 measurements |
| Blender packer on an organic character: 55-62% | 16 px gap at 2K | measured on the sheep (several runs) |
| inverted texels: single pass 7.3-7.8%, adaptive 2.2% (sculpt) / 1.2% (consistent high) | selected to active, sheep | measured |
| 2048 bake, 186k-face high: one pass normal + AO 4.4 s, adaptive 25 s | Metal GPU, headless | measured |
Quality gates
Code: q = uv_qa(obj, res), then uv_verdict(q, "game" or "film", priorities=...). unwrap()["unsolved_faces"] and ["folding_islands"]. measure_projection(). bake_high_to_low()["sanity"] (inverted, exact-flat, non-unit, AO black and white). Cross-check overlaps with bpy.ops.uv.select_overlap() when in doubt; it agreed with the raster on the tests.
Visual: layout PNG (islands, magenta overlaps), color-grid checker from front, side, three-quarter and the production camera (squares square, letters upright, seams hidden), angle and density heat maps, bake_review sheet (high, low, low + maps), map_problem_review when sanity flags pixels.
Common mistakes
| Mistake | Looks like | Fix |
|---|---|---|
uv.unwrap() without method | Conformal area distortion | pass method= or use unwrap(method="BEST") |
| Trusting FINISHED | closed island keeps its old UVs, repacked | zero the UVs first, check for zero-area faces (unwrap() does it) |
| Re-unwrapping without Seams from Islands | layout lost; retopology marks used as seams | seams_from_uv_islands first |
| Cutting every island to a disk | slits, folded islands | measured cut_to_disks (default) |
| Pack rotation ANY after straightening | tilted grids, jaggy lines | CARDINAL |
| Blender margin 0.005 | padding depends on island scale | FRACTION, gap/(2*res) |
| Target node active but not selected | bake returns CANCELED, no error | select and activate; assert FINISHED |
| Low carries the sculpt as Multires | almost flat normal map | render levels 0 for the bake (bakehighto_low does it) |
| max_ray shorter than extrusion | 90% exactly flat map | ray = extrusion + inward depth |
| One global extrusion on thin, close parts (ears on a head) | inverted patches, neighbors baked in | adaptive bake (default), explode, or per-pair bakes |
| Other objects render-visible during AO | black AO | hide them from render; the active low never occludes |
| Default AO distance 10 | AO all dark or flat | about 0.2 x asset diagonal |
| Stacked UVs baked in 0-1 | one side's detail on both | unique UVs or offset_stacked |
| Hard edge inside an island | crack lines | seam it, or prepare_low(sharp_from_uv_islands=True) |
| Editing UVs after the bake | seams, tangent mismatch | never; re-bake |
Blender 5.2 notes
uv.unwrapdefaults to CONFORMAL. On failure it keeps the island's old UVs and repacks them. It hasuse_weights/weight_group(default group "uv_importance"): importance weights with Minimum Stretch. A weighted top half went from 31% to 74% of UV area [verified].- Sync select is on by default (5.0).
BMLoopUVonly hasuvandpin_uv. Ame.uv_layers[...]reference goes stale after an Edit Mode toggle, so keep the name. pack_islands(udim_source="CLOSEST_UDIM")headless packed a region sitting in 1002 back into 1001: pack first, then move.uv.move_on_axisneeds a UV editor.- Bake: the target must be selected AND active (5.0). Operator defaults differ from scene defaults (
use_clearFalse vs True, EXTEND vs ADJACENTFACES): pass everything. Selected to Active uses the low's render evaluation (Multires renderlevels, shape keys). Multires settings live inscene.render.bake.use_multireswithobject.bake_image(). A float image saved as PNG is 16-bit. Normal Map nodeconventionOPENGL/DIRECTX (5.1). UDIM tiles needimage.tile_add(fill=True). - Auto Smooth is gone (4.1): smooth shading plus sharp edges; Triangulate "Keep Normals" is
keep_custom_normals.
References
references/expert-notes.md: judgment by expert with timestamps, disagreements and deciding conditions, my 5.2.1 measurements. Load when choosing seams, density or a bake strategy.- [`references/procedures.md`](references/procedures.md): tested pipelines (hard-surface, character, film/UDIM, Multires, bake-fix loop) and raw bpy snippets executed by the tests. Load before writing code.
references/critique.md: rubric for seams, stretch, density, packing, bake setup and baked maps, with proxies and severity. Load at every gate.- [`references/sources.md`](references/sources.md): videos, credentials, URLs, best timestamps, web references.
scripts/bx_uvbake.py: the module (docstrings list every function).