references/forms-and-bake-sources.md
# Forms, proportions, hard-surface sculpting, bake sources (operators verified in Blender 5.2.3)
## Human proportions (art canon, real scale in m)
| | Value |
|---|---|
| Height | adult ~1.75 m (male) / ~1.62 m (female). Model at real scale |
| Heads tall | realistic 7.5 · heroic 8–8.5 · stylized 5–7 · anime teen 6.5–7 · chibi 2–4. Head ≈ 0.22–0.23 m |
| Vertical landmarks | half height = pubis/greater trochanter. Nipples ≈ 2 heads from the top, navel ≈ 3, crotch ≈ 4. Knee ≈ halfway hip → sole |
| Arms | span ≈ height. Elbow at the waist, wrist at the crotch, fingertips mid-thigh (arms down) |
| Widths | shoulders ≈ 2 heads (male), 1.75 (female). Hips ≈ 1.5 (male), 1.75–2 (female) |
| Hand / foot | hand ≈ face height (chin → hairline). Foot ≈ forearm length (~0.26 m) |
| Face | eyes at half the head height, one eye-width apart. Nose base halfway brow → chin. Ears from brow to nose base. Mouth corners under the pupils |
Check proportions in an orthographic front/side view against a 1.75 m reference before any detail. Bad primary forms can't be saved by tertiary detail.
## Silhouette and form order
1. **Gesture/primary**: the big masses (ribcage, pelvis, skull as boxes/eggs), tilts and rhythm. They should read at 5% of screen.
2. **Secondary**: muscle groups, fat pads, bony landmarks (clavicle, iliac crest, kneecap, elbow), cloth folds. Work from planes, not blobs: flatten with `PLANE`, then soften.
3. **Tertiary**: skin pores, wrinkles, scratches, weave. Use Multires level 4–6 or (for games) normal/displacement maps from alphas.
Sculpt from reference, check every few minutes in a flat matcap from several angles, and turn the model upside down/mirrored to see errors fresh.
## Hard-surface sculpting (concepts, kitbash, organic-mechanical)
- **Trim gestures** cut or extrude crisp shapes: `sculpt.trim_box_gesture` / `trim_lasso_gesture` / `trim_line_gesture` (`trim_mode` DIFFERENCE/UNION/JOIN, `trim_solver` EXACT/FLOAT/MANIFOLD). `sculpt.project_line_gesture` flattens to a line.
- **Masks to parts**: paint a mask (or `sculpt.mask_from_cavity`), then `mesh.paint_mask_extract` (new shell object with thickness) or `mesh.paint_mask_slice` (split). `sculpt.face_set_extract` does the same per face set. Ideal for armor plates on a body.
- **Panels**: Face Sets (`sculpt.face_sets_init` by loose parts/normals/UV seams) + `sculpt.expand` for clean borders. Then `sculpt.mesh_filter(type="SHARPEN"|"RELAX_FACE_SETS"|"SURFACE_SMOOTH")` to crisp or relax along them.
- **Remesh in between**: `object.voxel_remesh` fuses cuts. 5.2 interpolates color attributes during voxel remesh, which is less scrambling but a bit blurrier.
- For production game props, hard-surface is usually polygon modeling (`blender-hardsurface-modeling`). Sculpt hard-surface for concepts and organic/mechanical blends.
## Multires tricks
- `object.multires_rebuild_subdiv` recovers subdivision levels from an imported dense mesh that came from a subdivided base. `object.multires_unsubdivide` finds a lower base from a dense uniform mesh.
- `object.multires_reshape` moves the detail of one mesh onto another with the same base topology (e.g. after a retopo with identical base).
## Preparing a high-poly bake source (→ `blender-texture-baking`)
- **Watertight where rays land**: no holes, no inverted parts (run `blender-asset-qa` on the high with `--profile render`). Overlapping floaters are fine (they bake as surface detail), interpenetrating thin shells are not (AO and normal leaks).
- **Density**: at least ~1 high-poly face per texel of the target map over the region. A 2k map on a 1 m prop needs about 2–4 M faces at the detail. Decimate the high (`Decimate` collapse 0.2–0.4) before baking to cut memory: normals survive it.
- **Same silhouette**: the low must envelope the high within the cage distance (1–2% of the object size). Bevel or chamfer high-poly edges so normals have something to capture (0.5–2 mm on props).
- **Naming for bake-by-name**: `Crate_high` / `Crate_low`, one pair per exploded part. Explode overlapping parts (move apart) before baking, or use bake groups, so the AO/normal of one part doesn't project onto another.
- Apply symmetry and scale. Keep the high in its own collection, excluded from export.
- External bakers (Marmoset, Substance): export the high as `.ply`/`.fbx` with smooth normals, triangulated low with tangents (`blender-export-pipelines`).
scripts/_bpyx.py
"""_bpyx - tiny shared helpers for blender-agent-skills scripts (Blender 5.2+).
Source of truth: shared/_bpyx.py. Copies in skills/*/scripts/ are synced by
tools/sync_shared.py - never edit a copy.
Every skill script follows one contract:
run(**kwargs) -> dict # call from MCP / Python (import the module)
blender -b f.blend -P x.py -- --arg v # CLI: prints one line "@@JSON@@{...}"
"""
import argparse
import json
import sys
import bpy
MARK = "@@JSON@@"
def argv():
"""Arguments after '--' (Blender ignores them)."""
return sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
def _default(o):
if hasattr(o, "__len__") and not isinstance(o, str):
try:
return [round(v, 5) if isinstance(v, float) else v for v in o]
except TypeError:
pass
if isinstance(o, (set, frozenset)):
return sorted(o)
return str(o)
def emit(data):
"""Print compact JSON on one marked line (parsed by run_in_blender.py)."""
print(MARK + json.dumps(data, separators=(",", ":"), default=_default), flush=True)
return data
def parser(doc):
"""ArgumentParser whose help is the script docstring's first line."""
return argparse.ArgumentParser(description=(doc or "").strip().splitlines()[0])
def cli(run, p):
"""CLI entry: parse args after '--', run, emit JSON. Exit 1 on failure."""
args = p.parse_args(argv())
try:
result = run(**vars(args))
save = getattr(args, "save", None)
if save:
bpy.ops.wm.save_as_mainfile(filepath=bpy.path.abspath(save))
result["saved"] = save
emit(result)
except Exception as e: # report, don't dump a traceback into agent context
emit({"ok": False, "error": f"{type(e).__name__}: {e}"})
if bpy.app.background:
sys.exit(1)
def add_common(p, save=True):
"""--objects / --save flags shared by most scripts."""
p.add_argument("--objects", nargs="*", default=None,
help="object names (default: selected, else all meshes)")
if save:
p.add_argument("--save", default=None, help="save .blend to this path after run")
return p
def objects(names=None, types=("MESH",)):
"""Resolve names -> objects. No names: selected of `types`, else all visible of `types`."""
if names:
missing = [n for n in names if n not in bpy.data.objects]
if missing:
raise KeyError(f"objects not found: {missing}")
return [bpy.data.objects[n] for n in names]
pool = [o for o in bpy.context.scene.objects if not types or o.type in types]
sel = [o for o in pool if o.select_get()]
return sel or [o for o in pool if o.visible_get()]
def object_mode():
"""Leave edit/sculpt/pose modes; most data API work needs OBJECT mode."""
ob = bpy.context.view_layer.objects.active
if ob and ob.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
def select_only(objs, active=None):
"""Select exactly `objs` and make `active` (default first) active."""
for o in bpy.context.view_layer.objects:
o.select_set(False)
for o in objs:
o.select_set(True)
bpy.context.view_layer.objects.active = active or (objs[0] if objs else None)
def node(tree, type_):
"""First node of `type_` (e.g. 'BSDF_PRINCIPLED'). Names are localized - never look up by name."""
return next((n for n in tree.nodes if n.type == type_), None)
def enum_ids(owner, prop):
"""Valid identifiers of an enum property (works on types or instances)."""
return [i.identifier for i in owner.bl_rna.properties[prop].enum_items]
def set_enum(owner, prop, value):
"""Assign an enum; on failure raise listing valid values (dynamic enums under-report)."""
try:
setattr(owner, prop, value)
except TypeError as e:
raise ValueError(f"{prop}={value!r} invalid: {e}") from None
def mesh_tris(ob, evaluated=True):
"""Triangle count (evaluated = with modifiers)."""
if evaluated:
eo = ob.evaluated_get(bpy.context.evaluated_depsgraph_get())
me = eo.to_mesh()
try:
me.calc_loop_triangles()
return len(me.loop_triangles)
finally:
eo.to_mesh_clear()
me = ob.data
me.calc_loop_triangles()
return len(me.loop_triangles)
def r(v, n=4):
"""Round a float or a vector for compact JSON."""
if isinstance(v, float):
return round(v, n)
return [round(x, n) for x in v]
scripts/sculpt_ops.py
"""Headless-safe sculpt preparation and procedural "sculpt" ops (no Sculpt Mode - it crashes in -b).
Ops run in the order given:
remesh=SIZE voxel remesh (uniform topology, fuses parts) e.g. remesh=0.01
symmetrize=+X mirror one side onto the other (+X|-X|+Y|-Y|+Z|-Z)
multires=LEVELS add Multires and subdivide (Catmull-Clark) LEVELS times
inflate=DIST push surface along normals (m)
detail=STR,SCALE noise displacement (organic surface breakup), e.g. detail=0.01,0.05
smooth=ITER volume-preserving Laplacian smooth
apply apply all modifiers except Multires
Usage:
blender -b f.blend -P sculpt_ops.py -- --objects Blob --ops remesh=0.02 symmetrize=+X multires=3 [--save out.blend]
MCP: import sculpt_ops; print(sculpt_ops.run(objects=["Blob"], ops=["remesh=0.02", "smooth=5"]))
"""
import os
import sys
import bpy
sys.path.insert(0, os.path.dirname(__file__))
import _bpyx as bx # noqa: E402
def _ctx(ob):
return bpy.context.temp_override(object=ob, active_object=ob, selected_objects=[ob],
selected_editable_objects=[ob])
def _apply(ob, keep_multires=True):
names = [m.name for m in ob.modifiers if not (keep_multires and m.type == "MULTIRES")]
for name in names: # names: applied modifiers are freed
with _ctx(ob):
bpy.ops.object.modifier_apply(modifier=name)
def run(objects=None, ops=None, **_):
bx.object_mode()
out = {}
for ob in bx.objects(objects):
log = []
for op in ops or []:
k, _, v = op.partition("=")
if k == "remesh":
_apply(ob, keep_multires=False)
ob.data.remesh_voxel_size = float(v)
ob.data.use_remesh_preserve_volume = True
with _ctx(ob):
bpy.ops.object.voxel_remesh()
elif k == "symmetrize":
axis = {"+X": "POSITIVE_X", "-X": "NEGATIVE_X", "+Y": "POSITIVE_Y", "-Y": "NEGATIVE_Y",
"+Z": "POSITIVE_Z", "-Z": "NEGATIVE_Z"}[v or "+X"]
bx.select_only([ob])
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.symmetrize(direction=axis)
bpy.ops.object.mode_set(mode="OBJECT")
elif k == "multires":
m = next((m for m in ob.modifiers if m.type == "MULTIRES"), None) or ob.modifiers.new("Multires", "MULTIRES")
for _ in range(int(v)):
with _ctx(ob):
bpy.ops.object.multires_subdivide(modifier=m.name, mode="CATMULL_CLARK")
m.levels = m.sculpt_levels = m.render_levels = m.total_levels
elif k == "inflate":
d = ob.modifiers.new("Inflate", "DISPLACE")
d.mid_level, d.strength = 0.0, float(v)
elif k == "detail":
strength, scale = (float(x) for x in v.split(","))
tex = bpy.data.textures.new(f"{ob.name}_detail", "CLOUDS")
tex.noise_scale, tex.noise_depth = scale, 4
d = ob.modifiers.new("Detail", "DISPLACE")
d.texture, d.strength, d.texture_coords = tex, strength, "LOCAL"
elif k == "smooth":
s = ob.modifiers.new("Smooth", "LAPLACIANSMOOTH")
s.iterations, s.lambda_factor, s.use_volume_preserve = int(v), 0.5, True
elif k == "apply":
_apply(ob)
else:
raise ValueError(f"unknown op {op}")
log.append(op)
bpy.context.view_layer.update()
mr = next((m for m in ob.modifiers if m.type == "MULTIRES"), None)
out[ob.name] = {"ops": log, "base_faces": len(ob.data.polygons),
"evaluated_tris": bx.mesh_tris(ob), "multires_levels": mr.total_levels if mr else 0,
"stack": [m.name for m in ob.modifiers]}
return {"ok": True, "objects": out}
if __name__ == "__main__":
p = bx.add_common(bx.parser(__doc__))
p.add_argument("--ops", nargs="*", default=[])
bx.cli(run, p)
SKILL.md
---
name: blender-sculpting
description: Sculpting in Blender 5.2 - base-to-detail workflow, voxel remesh, dyntopo, multires, 5.x brush types, masks/face sets, symmetry, plus headless-safe procedural ops for agents (remesh, symmetrize, multires, inflate, noise detail, smooth) and the sculpt-to-game handoff. Use for organic models, characters, creatures, rocks and bake sources.
license: MIT
metadata:
author: blender-agent-skills
version: "1.2.0"
---
# Sculpting
The execution model is in `blender-core`. The handoff goes to `blender-retopology-uv` → `blender-texture-baking`.
## Agents: what you can and can't do
- **Sculpt Mode operators crash Blender in background mode** (`-b`), including `mesh_filter`. Never enter Sculpt Mode headless.
- Headless or MCP: use `scripts/sculpt_ops.py` for object-level ops: `remesh=SIZE`, `symmetrize=+X`, `multires=N`, `inflate=D`, `detail=STR,SCALE`, `smooth=N`, `apply`. Combine these with Geometry Nodes (`blender-geometry-nodes`: SDF grid booleans/fillets, noise displacement) and shape modeling for procedural organic forms.
- Live sessions with a UI: brush strokes via `bpy.ops.sculpt.brush_stroke(stroke=[...])` need a 3D View region override and are fragile. Prefer procedural ops, then show the user with `look`. Hand detailed brushwork to the human.
## Professional workflow
1. **Base mesh / blockout**: primitives, metaballs, Skin modifier, or a low-poly base at real scale. Proportions first: compare with references and check against a human 1.75 m.
2. **Unify**: voxel remesh (`remesh=0.02` for a 2 m character, smaller for detail) fuses parts and gives even density. Repeat it while the big forms change.
3. **Primary forms** at low resolution (Grab, Snake Hook, Clay Strips, Inflate, Smooth). **Secondary** (muscle groups, folds) at medium resolution. **Tertiary** (pores, scratches, fabric weave) with Multires levels 4–6, or as normal/displacement maps.
4. **Dynamic topology** (`sculpt.dynamic_topology_toggle`): adds detail where you stroke. Good for concept sculpting, bad for final topology and multires (it destroys UVs/multires). Use voxel remesh + multires for production.
5. **Multires**: subdivide (`multires_subdivide`, Catmull-Clark) on a clean quad base (after retopo) to sculpt high detail that bakes back to the base level (`blender-texture-baking`, multires bake or high→low).
6. **Handoff**: export the high (or keep it in-file), retopo to `_low`, UV, bake normal/AO/curvature, then texture.
## Brushes (5.x: brushes are assets in the Essentials library)
| Purpose | `sculpt_brush_type` |
|---|---|
| Build volume | `CLAY_STRIPS`, `CLAY`, `CLAY_THUMB`, `LAYER`, `INFLATE`, `BLOB` |
| Shape / move | `GRAB`, `SNAKE_HOOK`, `ELASTIC_DEFORM`, `POSE`, `BOUNDARY`, `CLOTH`, `NUDGE`, `ROTATE`, `THUMB` |
| Details / edges | `DRAW`, `DRAW_SHARP`, `CREASE`, `PINCH` |
| Flatten / hard surface | `PLANE` (flatten/fill/scrape), `MULTIPLANE_SCRAPE` |
| Clean up | `SMOOTH`, `SIMPLIFY` (dyntopo), `DISPLACEMENT_ERASER`/`SMEAR` (multires) |
| Masking / grouping | `MASK`, `DRAW_FACE_SETS`; Expand (`sculpt.expand`), mask from cavity |
5.0 renamed `brush.sculpt_tool` → `sculpt_brush_type`. 5.1 folded the stroke options into `brush.stroke_method` (`DOTS`, `DRAG_DOT`, `SPACE`, `AIRBRUSH`, `ANCHORED`, `LINE`, `CURVE`). 5.2 moved automasking to `brush.mesh_automasking_settings` / `paint.mesh_automasking_settings`.
## Rules of thumb
- Sculpt at **real scale**, with scale applied. Brush radius is in screen space but strength and remesh sizes depend on scale.
- Use symmetry (`mesh.use_mirror_x`, radial: `mesh.radial_symmetry`) on characters until the asymmetry pass at the end.
- Face Sets organise parts (head, torso, hands). Hide them and mask by face set to isolate.
- Watch density: a 2 m character at voxel 0.005 is already millions of polys. Keep viewport FPS usable and work up in levels.
- Silhouette first. Check from several angles in flat matcap (`look` shading `solid`) before adding detail.
- Floaters/kitbash for hard-surface details on organic models: separate objects, combined at bake time.
- Before baking, run `blender-asset-qa` on the high (no inverted parts, no huge self-intersections in the bake range).
## References
[references/forms-and-bake-sources.md](references/forms-and-bake-sources.md): human proportions in meters and heads, form order (primary → tertiary), hard-surface sculpting (trim gestures, mask extract, face sets, mesh filters), multires rebuild/reshape, and preparing a high-poly bake source (density, cage, naming, exploding). Load it before character, creature or bake-source work.
## Gotchas
- `voxel_remesh` destroys UVs, vertex groups (unless preserve attributes) and multires levels. Do it before UVs/rigging.
- Multires + topology changes don't mix: never edit the base topology after subdividing (apply the base or rebuild).
- `symmetrize` on a non-centered object mirrors across the **object's local axis**. Center the origin on the symmetry plane first.
- Sculpting on shape keys: enable `use_shape_key_edit_mode` and sculpt on the active key (facial work, `blender-facial-shapekeys`).