---
name: uv_unwrapping_basics
description: "Unwrap meshes in Blender: place and clear seams, choose an unwrap or projection method, fix stretching with a checker texture, keep texel density consistent, pack islands, handle UDIM tiles, and build a second UV channel for lightmaps. Triggers on a model that needs UVs before texturing, stretched or blurry textures, seams showing in the wrong place, overlapping islands, or a lightmap UV request."
metadata: { "includeInPrompt": true }
---

# UV Unwrapping Basics

## Purpose

Give a mesh a UV layout that textures and bakes can use without visible stretching, seams in prominent places, or density that changes from part to part. This skill owns unwrap craft itself: seams, islands, stretching checks, texel density, packing, UDIM basics, lightmap UVs.

Cousin skills, one line each:

- blender-material-shaders builds PBR materials with shader nodes and touches UV work only for texturing; this skill is the unwrap that has to happen before that texturing reads well.
- gltf-asset-optimization is delivery prep for web models; it consumes UVs that already work and does not teach how to make them.
- headless-blender is rendering; it runs Blender from the command line, which this skill uses only to run its audit script.

## Workflow

1. Check what already exists before unwrapping anything. In the mesh data, list the UV layers and note which one is active and which one is set as the render or export layer. Run the audit script in step 10 on the starting file. A mesh with no UV layer, or an object the user forgot had a second layer, changes the whole job. Save a copy of the file before editing UVs.
2. Plan seams before marking any. A seam is a cut the unwrap opens along, so put cuts where a texture break is least visible or most expected:
   - Along hard edges and material boundaries, where a break already reads as a panel line or trim change.
   - On the underside, back, inside, and other low-view areas of the model.
   - Along the shortest path that lets a tube or shell flatten, for example down the back of a limb or the inside of a handle, not around its front face.
   - Away from faces, logos, text, and any surface that needs one unbroken texture across it.
   Use fewer, longer seams on organic shapes and seam along most hard edges on hard-surface models. Every seam is a potential visible line and a potential baking artifact, so each one needs a reason.
3. Mark and clear seams in Edit Mode with edges selected. In Python the verified calls are `bpy.ops.mesh.mark_seam(clear=False)` to mark and `bpy.ops.mesh.mark_seam(clear=True)` to clear. Verified on Blender 5.1.1: there is no separate `bpy.ops.uv.clear_seam` operator, clear is the `clear=True` argument on mark_seam. The UV Editor has its own `bpy.ops.uv.mark_seam(clear=False)` / `clear=True` pair for work done from the UV side. Confirm menu labels in your installed version if you work from the UI instead of script.
4. Pick an unwrap method per part, not one method for the whole model:
   - Unwrap (`bpy.ops.uv.unwrap`): the main method. Uses the seams from step 2 and flattens each island. Use it for anything with deliberate seams. Useful verified properties: `method`, `fill_holes`, `correct_aspect`, `margin`.
   - Smart UV Project (`bpy.ops.uv.smart_project`): cuts by angle with no manual seams. Use it for fast work on hard-surface props, kitbash parts, and anything headed for procedural textures or a trim sheet. Verified properties: `angle_limit` (66 degrees is a sane starting value), `island_margin`, `area_weight`, `correct_aspect`, `scale_to_bounds`. Expect many small islands and seams in visible places, so do not use it as the final unwrap for a hero organic model.
   - Cube, Cylinder, and Sphere Projection (`bpy.ops.uv.cube_project`, `bpy.ops.uv.cylinder_project`, `bpy.ops.uv.sphere_project`): project from a simple shape. Use them when the part matches the shape, for example a can, a bottle body, a ball, a crate. Check the poles and the wrap edge after sphere and cylinder projections, since that is where stretching and the seam concentrate.
   - Follow Active Quads (`bpy.ops.uv.follow_active_quads`, verified property `mode`, with `LENGTH_AVERAGE` as a mode value): extends a regular grid from one active quad across a quad-based mesh. Use it for cloth, grids, fences, building panels, and any surface where straight texture lines must stay straight. It needs clean quad topology and one well-shaped active face to start from.
   Start with Unwrap for seamed parts, fix the parts it handles badly with a projection or Follow Active Quads, and keep each part on the method that gives it the least stretching.
5. Work the islands. In the UV Editor, select one island with linked selection (`bpy.ops.uv.select_linked` exists for this) so moves, rotations, and scales apply to the whole island, never to stray faces. Straighten islands that carry straight texture features, align long islands to U or V, and rotate an island only with a reason, since rotated islands fight tiling textures. Stitch islands back together where a seam was cut too eagerly: `bpy.ops.uv.stitch` exists, with verified properties including `use_limit`, `limit`, `snap_islands`, and `clear_seams`. Stitching joins UV vertices across a cut and can reintroduce stretching, so re-run the checker check in step 6 after any stitch. Use Seams from Islands (`bpy.ops.uv.seams_from_islands`) when an existing layout is good and you need matching mesh seams for a re-unwrap.
6. Check stretching with a checker texture before packing. Create a test image and put it on the model through a temporary material or as the UV Editor background, then look at the model from all sides:
   - In script: `img = bpy.data.images.new("Checker", width=1024, height=1024)`, then set `img.generated_type` to `'COLOR_GRID'` for a grid with letters and colored crosses. That setting was run in script on 5.1.1 and worked. The plain grid option is `'UV_GRID'`; confirm it in your version before relying on it.
   - Squares that stay square and the same size mean the layout is even. Rectangles mean directional stretch. Squares that change size from part to part mean density changes, which step 7 fixes. Heavy distortion concentrated near a seam or a pole means the cut or the projection is wrong there: move the seam, change the method for that part, or add a cut to relieve it.
   Do not judge stretching from the flat UV view alone. A layout that looks tidy in 2D can still stretch badly on the model.
7. Make texel density consistent. With all islands for one texture selected, run Average Island Scale (`bpy.ops.uv.average_islands_scale`, verified properties `scale_uv` and `shear`) so islands take UV space in proportion to their surface area. Then scale the whole set together to hit the target density for the project. Work out the target in numbers:
   - Density proxy from the audit script is UV area divided by surface area.
   - Approximate texels per world unit at texture size T is T times the square root of the proxy.
   - Worked check from the fixture in step 10: the Cube proxy is 0.0156, square root 0.125, so at 2048 px it gets about 256 texels per world unit. The Ball proxy is 0.0752, square root about 0.274, so at 2048 px it gets about 562 texels per world unit. Side by side in one scene at one texture size, the Ball would read roughly twice as sharp as the Cube. That mismatch is what this step removes.
   Parts that need close-up detail may deliberately run denser than the rest. Record that as a decision, not an accident.
8. Pack islands after scale is settled. Run Pack Islands (`bpy.ops.uv.pack_islands`) with verified properties including `rotate`, `scale`, `merge_overlap`, `margin`, and `margin_method`. Set a margin in UV terms, not by eye: at 2048 px, a margin of 0.003 is about 6 px between islands, which is a workable starting gap for bakes that dilate a few pixels. Tighten or widen it to the bake padding the downstream step uses. Leave `rotate` on for organic layouts to gain space, and turn it off when grain, weave, brushed metal, or any directional texture must run one way. After packing, confirm no islands overlap unless the overlap is a deliberate mirror or repeat: overlapping UVs share texture and break unique bakes and lightmaps.
9. Handle UDIM only when the project needs more than one tile. UDIM spreads one texture set across tiles in a grid: tile numbers start at 1001 for the U 0, V 0 tile, increase by 1 per step in U, and by 10 per step in V, so U 1, V 0 is 1002 and U 0, V 1 is 1011. Image files carry the number in the name, in the shape `name.1001.png`, `name.1002.png`. Rules for UDIM work:
   - One island lives on one tile. An island split across a tile border samples the wrong files.
   - Pack per tile, not across the whole grid, and keep density consistent across tiles using step 7.
   - Give hero areas their own tile instead of enlarging everything.
   - Confirm the target renderer or viewer actually loads UDIM sets before committing. Many simple web viewers and game import paths expect a single 0 to 1 layout, so a UDIM model may need a separate single-tile version for them.
10. Audit the result with a script. Save the script below as `uv_audit.py`, then run `blender -b model.blend --python uv_audit.py` with your Blender binary. On this machine that binary is `~/.local/bin/blender`, version 5.1.1. The script reports, per mesh object and per UV layer, face count, surface area, UV area, the density proxy from step 7, the U and V ranges, and which tiles face centers fall on.

    ```python
    #!/usr/bin/env python3
    """Report UV layers, coverage, tiles, and a texel-density proxy per mesh object.

    Run inside Blender:
      blender -b model.blend --python uv_audit.py
    Prints one block per mesh object, per UV layer. The density proxy is
    UV area divided by mesh surface area: objects that share one texture
    should have similar proxies. It is a comparison number, not an
    absolute texel density (that needs the texture resolution).
    """
    import bpy
    from math import floor


    def uv_face_area(points):
        # Shoelace area of a UV polygon given as (u, v) tuples.
        total = 0.0
        n = len(points)
        for i in range(n):
            u1, v1 = points[i]
            u2, v2 = points[(i + 1) % n]
            total += u1 * v2 - u2 * v1
        return abs(total) / 2.0


    def main():
        meshes = [o for o in bpy.context.scene.objects if o.type == "MESH"]
        if not meshes:
            print("no mesh objects in scene")
            return
        for obj in sorted(meshes, key=lambda o: o.name):
            me = obj.data
            world_area = sum(p.area for p in me.polygons)
            layer_names = [layer.name for layer in me.uv_layers]
            print(f"object: {obj.name}  faces: {len(me.polygons)}  "
                  f"surface_area: {world_area:.4f}  uv_layers: {layer_names}")
            if not layer_names:
                print("  NO UV LAYER")
                continue
            for layer in me.uv_layers:
                uv_area = 0.0
                us, vs, tiles = [], [], set()
                for poly in me.polygons:
                    pts = []
                    for li in poly.loop_indices:
                        uv = layer.data[li].uv
                        pts.append((uv.x, uv.y))
                        us.append(uv.x)
                        vs.append(uv.y)
                    uv_area += uv_face_area(pts)
                    if pts:
                        cu = sum(pt[0] for pt in pts) / len(pts)
                        cv = sum(pt[1] for pt in pts) / len(pts)
                        tiles.add((floor(cu), floor(cv)))
                proxy = (uv_area / world_area) if world_area > 0 else 0.0
                print(f"  layer: {layer.name}  active: {layer.active}  "
                      f"uv_area: {uv_area:.4f}  density_proxy: {proxy:.4f}  "
                      f"u: {min(us):.3f}..{max(us):.3f}  v: {min(vs):.3f}..{max(vs):.3f}  "
                      f"tiles: {sorted(tiles)}")


    if __name__ == "__main__":
        main()
    ```

    Real output from a fixture built for this skill on Blender 5.1.1 (a default Cube given a second layer named LightMap, plus a UV sphere primitive renamed Ball):

    ```text
    object: Ball  faces: 512  surface_area: 12.4657  uv_layers: ['UVMap']
      layer: UVMap  active: True  uv_area: 0.9375  density_proxy: 0.0752  u: 0.000..1.000  v: 0.000..1.000  tiles: [(0, 0)]
    object: Cube  faces: 6  surface_area: 24.0000  uv_layers: ['UVMap', 'LightMap']
      layer: UVMap  active: True  uv_area: 0.3750  density_proxy: 0.0156  u: 0.125..0.875  v: 0.000..1.000  tiles: [(0, 0)]
      layer: LightMap  active: False  uv_area: 0.3750  density_proxy: 0.0156  u: 0.125..0.875  v: 0.000..1.000  tiles: [(0, 0)]
    ```

    Read the audit for: objects with NO UV LAYER, U or V ranges outside 0 to 1 on a job that is not UDIM, tiles the job did not plan for, and density proxies that differ across objects meant to share a texture.
11. Build the second UV channel for lightmaps when the target needs baked lighting. Add a new UV layer with a clear name such as LightMap and make it active before unwrapping, so the texture UVs stay untouched. Lightmap UVs follow different rules from texture UVs:
    - No overlapping islands at all, including mirrored parts. Overlap in a lightmap means two surfaces share baked light and one of them will be wrong.
    - Every face gets its own space with margin around it, since light leaks across tight gaps.
    - Density and clean shapes matter less than separation. Angle-based cutting is acceptable here even when it was not acceptable for texture UVs.
    The dedicated operator exists: `bpy.ops.uv.lightmap_pack`, with verified properties including `PREF_NEW_UVLAYER`, `PREF_PACK_IN_ONE`, `PREF_BOX_DIV`, and `PREF_MARGIN_DIV`. A plain Smart UV Project followed by Pack Islands with a generous margin reaches the same result and is easier to inspect. After building the layer, set the texture layer back to active and confirm which layer is flagged for export, so the lightmap channel is not mistaken for the texture channel.
12. Prepare the export. Check these before handing the file on:
    - Layer names are stable and meaningful. Downstream materials and engines select UV channels by name or by order, and a rename after materials are built breaks the link.
    - The correct layer is active and flagged as the one the exporter should treat as primary, per the target format settings. Confirm those settings in the exporter in your version instead of assuming defaults.
    - UDIM jobs export as a file set with the tile numbers in the names from step 9, and the receiving side has the full set. A missing tile file shows up as a blank or pink region, not as an error at export time.
    - glTF carries UV sets as numbered TEXCOORD attributes and many viewers read only the first set. If the model carries a lightmap channel or a UDIM layout headed for a web viewer, state that plainly in the handover and let gltf-asset-optimization handle delivery prep.
    - Modifiers that change geometry after the unwrap (subdivision level changes, booleans, remesh) can change or destroy the layout. Apply or settle them, then re-run the audit in step 10 on the exported form of the mesh.

## Output Contract

Report the result in this shape, with numbers from the model and the audit script, not estimates:

- File and objects worked on, with the UV layer names on each object before and after.
- Seam plan in one or two lines per object: where seams went and why, and any surface deliberately kept seamless.
- Method used per part (Unwrap, Smart UV Project, a named projection, Follow Active Quads), with the settings that matter, such as Smart UV Project angle limit and island margin.
- Checker check result: where stretching was found, what fixed it, and any distortion accepted and why. If no checker check happened, say so plainly.
- Texel density: target texels per world unit and texture size it assumes, the density proxy per object from the audit, and any part deliberately set denser.
- Pack result: margin used, rotation on or off and why, and overlap status (none, or which islands overlap on purpose).
- UDIM, if used: tile count, tile numbers and what lives on each, and confirmation that the target loads UDIM sets.
- Lightmap channel, if built: layer name, overlap status, and margin used.
- Audit output for the final file, or the lines for the objects changed.
- Export state: which layer is primary for export, layer names as exported, and any channel or UDIM limit flagged for the receiving side.

## Operating Rules

1. Never unwrap over the only copy. Work on a saved copy or a duplicated layer so a bad unwrap can be discarded.
2. Seams are a design decision, not a cleanup step at the end. Place them from the plan in step 2, and move them when the checker check shows the cost landing in the wrong place.
3. One method per part is normal. Forcing one projection or one Smart UV Project pass over a mixed model is the usual cause of stretching that no amount of packing fixes.
4. Judge the layout on the model with the checker applied, not in the flat UV view alone.
5. Average island scale before packing, and pack before judging wasted space. Packing first hides density errors by resizing islands to fit.
6. Overlap is a stated decision or a defect, never a surprise. It is fine for mirrored or repeated parts on texture UVs and never fine on lightmap UVs.
7. Keep the texture channel and the lightmap channel separate, named, and in a stable order across revisions. Renaming or reordering channels mid-project breaks downstream materials silently.
8. Do not invent operators, menus, or shortcuts. Verification basis for this skill, checked 2026-10-13 on Blender 5.1.1 at `~/.local/bin/blender` (build hash b70da489d7f4): a headless probe confirmed these operators exist by reading their RNA types: `bpy.ops.uv.unwrap`, `bpy.ops.uv.smart_project`, `bpy.ops.uv.cube_project`, `bpy.ops.uv.cylinder_project`, `bpy.ops.uv.sphere_project`, `bpy.ops.uv.follow_active_quads`, `bpy.ops.uv.pack_islands`, `bpy.ops.uv.average_islands_scale`, `bpy.ops.uv.stitch`, `bpy.ops.uv.seams_from_islands`, `bpy.ops.uv.select_linked`, `bpy.ops.uv.lightmap_pack`, `bpy.ops.mesh.mark_seam`, and `bpy.ops.uv.mark_seam`. The same probe found no `bpy.ops.uv.clear_seam` operator; clearing is `mark_seam(clear=True)`. Headless runs also executed cleanly on a test file: `mesh.mark_seam(clear=True)`, `uv.unwrap()`, `uv.average_islands_scale()`, `uv.pack_islands(margin=0.001)`, and `uv.smart_project(angle_limit=66.0, island_margin=0.003)`. Creating an image with `generated_type` set to `'COLOR_GRID'` also ran cleanly. Anything not listed here gets confirmed in the installed version before use, and that check is part of the job.
9. The audit script proxy is a comparison number. Convert it with a named texture size before calling anything a texel density, using the formula in step 7.
10. UDIM and second channels get confirmed against the receiving tool before the work is done, not after delivery. A correct layout the target cannot read is a failed delivery.
