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.
./install.sh uv-unwrapping-basicsname: 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 }
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:
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.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.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.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.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.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.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.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: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.name.1001.png, name.1002.png. Rules for UDIM work: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.
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.Report the result in this shape, with numbers from the model and the audit script, not estimates:
~/.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.publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e
No ratings yet — be the first to rate it.