Paint textures by hand in Blender: create or load a paint image, wire it into a material image texture slot, set brush size, strength, blend mode and falloff, paint in the 3D Viewport or the Image Editor with stroke methods and symmetry, choose a texture resolution, handle seam bleed, and save the image itself. Use when hand painting a base color, roughness, mask or other image texture on a mesh that already has UVs.
name: blender_texture_painting_basics description: "Paint textures by hand in Blender: create or load a paint image, wire it into a material image texture slot, set brush size, strength, blend mode and falloff, paint in the 3D Viewport or the Image Editor with stroke methods and symmetry, choose a texture resolution, handle seam bleed, and save the image itself. Use when hand painting a base color, roughness, mask or other image texture on a mesh that already has UVs." metadata: { "includeInPrompt": true }
Blender Texture Painting Basics
Purpose
Paint pixels by hand onto an image texture that a material uses, and get that image saved. The work has four parts that fail in different ways: the image has to exist with the right size and color space, the material has to hold that image in an image texture node linked to the right input, the brush and paint settings have to match the job, and the painted image has to be saved on its own. A mesh can look painted in the viewport and still hold no saved texture if any one of those parts was skipped.
Scope:
- blender-material-shaders: "Builds PBR materials with Blender's shader nodes; triggers when crafting Principled BSDF materials, procedural Noise/Voronoi textures, UV work for texturing, or choosing between normal maps and bump." Split: that skill builds materials from nodes and procedural textures; this skill paints pixels by hand onto an image texture.
- uv-unwrapping-basics: "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." Split: that skill ends with UVs ready; this skill starts once UVs exist.
- headless-blender covers headless rendering only; this skill uses headless runs only to audit paint setup, not to render and not to perform brush strokes.
- Verification note, 2026-10-20: paint_audit.py below was run headless with Blender 5.1.1 on the drafting machine. The fixture run returned blender_version: 5.1.1, paint_objects: 2, paint_slots: 2, images: 2, images_dirty: 1, and images_saved: 1, with PaintCube holding Cube_BaseColor at 256x256 in sRGB with dirty=True and no filepath, and DataCube holding Cube_Roughness at 128x128 in Non-Color with dirty=False and a saved PNG filepath. A default factory scene run returned paint_objects: 0, paint_slots: 0, and images: 0. Python is not used standalone here: the script runs inside Blender through Blender's own Python, and uses bpy from that session. What was NOT verified: actual brush strokes in the UI were not performed headless, because mode_set and painting strokes need an interactive context; the claim that saving the .blend file alone saves the image was not tested headless and is not made in this skill; and the emptiness of Material.texture_paint_images was observed in headless 5.1.1 runs only, so this skill states it as a headless observation, not as a general UI claim.
Workflow
- Confirm UVs exist before painting anything.
- Check the mesh has at least one UV layer and note its name. The audit prints uv_layers for each paint object; a fixture cube reports UVMap.
- If there is no UV layer, stop and unwrap first. Painting needs UVs to place pixels on the surface. The operators bpy.ops.uv.unwrap, bpy.ops.object.mode_set, bpy.ops.paint.texture_paint_toggle, bpy.ops.image.new, bpy.ops.image.open, bpy.ops.image.save, and bpy.ops.image.save_as all exist in Blender 5.1.1, but existence in a headless check does not prove an interactive stroke worked.
- Paint on the UV layer the material will use at render time. A second layer built for another purpose gives a clean looking paint job in the wrong place.
- Decide the map type, color space, and resolution before creating the image.
- Color maps, such as a base color map, use sRGB. Data maps, such as roughness, normal, and mask maps, use Non-Color. This split was verified as settable on an image in 5.1.1.
- Pick the smallest resolution that holds the detail the surface needs at its closest planned view. Larger images hold finer strokes and cost more memory and save time. The fixture uses 256x256 for a base color test and 128x128 for a roughness test; those are fixture sizes, not recommendations.
- Decide the file path and format at creation time when the image will be saved to disk. An image with no filepath has nowhere for a plain save to write to.
- One image per map type. Do not paint roughness detail into the base color image and expect the material to read it as roughness.
- Create or load the paint image.
- Create in a script with bpy.data.images.new(name, width, height, alpha=True). A new image defaults to sRGB in 5.1.1; set colorspace_settings.name to "Non-Color" for a data map.
- Create in the UI from the Image Editor with the new image action, setting name, width, and height in that dialog, then set the color space to match step 2.
- Load an existing file instead when painting over prior work: open the image file, confirm its size and color space in the audit, and confirm its filepath is the file you mean to overwrite or copy.
- Record the image name exactly as created. Later steps and the audit refer to images by that name.
- Wire the image into a material image texture slot.
- Give the object a material that uses nodes, add a ShaderNodeTexImage node, and set that node's image to the paint image from step 3.
- Link the node output to the matching Principled BSDF input: Color to Base Color for a color map, Color to Roughness for a roughness map. The fixture wires Cube_BaseColor to Base Color and Cube_Roughness to Roughness this way.
- Keep the image texture node as the active paint target for that material. Material.paint_active_slot exists in 5.1.1 and the fixture reports active_slot=0 for both materials.
- Note the audit limit plainly: paint slots in the audit are derived from ShaderNodeTexImage nodes because Material.texture_paint_images returned an empty list in headless 5.1.1 runs even with a TexImage node present and active. A slot the audit lists is a TexImage node with an image, found by walking the node tree.
- Audit the setup before painting.
- Save the script from the Operating Rules section as paint_audit.py and run it on the file inside Blender:
blender -b your_file.blend -noaudio --python paint_audit.py.
- Check each line: object name, uv_layers, material name, active_slot, image name, size, colorspace, dirty flag, and filepath. Fix a wrong size or color space now, before strokes go down.
- A missing object in the output means no material on that object holds a TexImage node with an image. Fix the slot in step 4 instead of painting and hoping the paint lands somewhere.
- Keep this first audit output with the work record. Later audits are compared against it to prove which image changed and which image was saved.
- Enter Texture Paint mode and set the paint target.
- Select the object and enter Texture Paint mode in the interactive UI. Painting strokes are interactive work; no headless run in this skill's verification performed a stroke.
- ToolSettings.image_paint has a mode property with two values, MATERIAL and IMAGE. In MATERIAL mode the paint target comes from the active material slot. In IMAGE mode the target is the image set on the canvas property, which can be set to an image.
- Set the mode to match how the slot was built in step 4, and if IMAGE mode is used, set canvas to the exact paint image from step 3.
- Confirm the image shown as the paint target matches the image name in the step 5 audit before the first stroke. Painting the wrong canvas is a common silent failure because the viewport still shows brush feedback.
- Set the brush basics: size, strength, blend mode, and falloff.
- A brush can be created in a script with bpy.data.brushes.new(name, mode='TEXTURE_PAINT'). Verified defaults in 5.1.1 are size 70, strength 1.0, blend MIX, stroke_method SPACE, and falloff_shape SPHERE.
- Size sets the brush radius on screen. Use a large size for broad coverage and a small size for edge and detail work, and change it as the area changes instead of forcing one size through the whole job.
- Strength sets how much each pass applies. Full strength suits flat fills; lower strength suits building up shading in passes that can be judged between strokes.
- Blend mode sets how a stroke combines with the pixels under it. Verified blend values include MIX, DARKEN, MUL, ADD, OVERLAY, SOFTLIGHT, ERASE_ALPHA, and ADD_ALPHA, among others. MIX is the plain paint over case; ERASE_ALPHA and ADD_ALPHA change alpha rather than color.
- Falloff sets how paint fades across the brush radius, with SPHERE as the verified default shape. The brush also has curve_size and curve_strength properties for the size and strength curves.
- ImagePaint.brush is read-only in 5.1.1. Do not write a script step that assigns the brush through ImagePaint.brush, and do not claim that assignment works.
- Choose a stroke method for the mark you need.
- Verified stroke_method values are DOTS, DRAG_DOT, SPACE, AIRBRUSH, ANCHORED, LINE, and CURVE.
- SPACE, the verified default, spaces dabs along the stroke as the pointer moves, which suits freehand painting.
- DOTS places dabs by distance with a different spacing feel; DRAG_DOT places one dab that drags with the pointer; AIRBRUSH keeps applying while the pointer stays down; ANCHORED holds the dab to its starting point while the pointer sets its size; LINE and CURVE constrain the stroke to a drawn line or curve.
- Pick the method per mark, and write the method used into the work record when a result needs to be reproduced.
- Set symmetry and seam bleed.
- ToolSettings.image_paint has use_symmetry_x, use_symmetry_y, and use_symmetry_z, all False by default in 5.1.1. Turn on only the axes the object is actually symmetric across; a wrong axis mirrors strokes onto parts that should differ.
- seam_bleed exists on the same settings and is 2 by default. Bleed extends paint past a UV island edge so a seam does not show unpainted or thin pixels where islands meet on the surface.
- Raise bleed when seams show a light line after painting, and check the result at the seam on the model. Too much bleed can push paint into a neighboring island that maps to a different part of the surface.
- Test symmetry and bleed on a small patch first, then commit to the full surface.
- Paint, choosing the 3D Viewport or the Image Editor per stroke.
- Paint in the 3D Viewport when placement on the surface matters: logos, wear at an edge, dirt in a crevice. The brush lands where the model is seen, and UV distortion is visible as it happens.
- Paint in the Image Editor when flat coverage matters: filling an island, painting a straight pattern, or working a mask. The stroke is placed in UV space, so check the model view after to see where it landed on the surface.
- Switch views on purpose during the job. A stroke that reads well flat can sit wrong on a curved surface, and a stroke placed on the model can leave a thin spot at an island edge that only the flat view shows.
- Check seams, mirrored areas, and stretched UV areas on the model as you go, not only at the end. Fixing a seam while the surrounding paint is fresh in mind is cheaper than repainting a finished area.
- Save the image itself, then re-audit.
- Painted work lives in the in-memory image until the image itself is saved, through the Image menu Save or Save As actions in the UI, or through image.save() in a script.
- Verify with the audit dirty flag: an image with dirty=True and no filepath will lose its painted pixels when the file is closed without saving the image. The fixture proves the flag side of this: Cube_BaseColor showed is_dirty True after a pixel change, and Cube_Roughness showed False after save.
- Prefer image.save() in scripts. It worked headless in 5.1.1: after pixels were assigned, is_dirty was True; after save() to a PNG path, is_dirty was False and the file existed. In contrast, bpy.ops.image.save_as failed its poll in the headless factory context, so do not build a headless save step on that operator.
- Saving the .blend file alone is not presented in this skill as saving the image, because that specific claim was not tested headless. Treat .blend saving and image saving as two separate steps until a check proves otherwise for your case.
- Re-run paint_audit.py after saving. The saved image should report dirty=False and the filepath you intended. An image still reporting dirty=True has not been saved, whatever the viewport shows.
- Review the painted result as a material, not only as an image.
- View the object with the full material applied and confirm each painted map drives the input it was wired to in step 4: base color reads as color, roughness reads as roughness variation, a mask reads where it is used.
- Check the surface at the closest planned viewing distance. Blurry paint at that distance means the resolution from step 2 was too low for the detail attempted; repainting at the same size will not fix that.
- Record the final state: image names, sizes, color spaces, filepaths, brush and stroke settings used for the main passes, symmetry axes used, seam bleed value, and the final audit output.
Output Contract
Deliver these items, in this order:
- Paint setup record: one entry per paint object with object name, UV layer name, material name, active slot, and the ShaderNodeTexImage node that holds the paint image.
- Image record: one entry per paint image with image name, size, color space (sRGB for color maps, Non-Color for data maps), filepath or a dash when it has none, and its dirty flag before and after saving.
- Audit output: the full output of paint_audit.py before painting and after saving. On the worked fixture in the Operating Rules section the headline numbers are: blender_version: 5.1.1, paint_objects: 2, paint_slots: 2, images: 2, images_dirty: 1, images_saved: 1.
- Brush record: brush name, size, strength, blend mode, falloff shape, and stroke method for the main passes, plus the symmetry axes enabled and the seam bleed value.
- Painting record: which maps were painted in the 3D Viewport and which in the Image Editor, and the seam, symmetry, and stretch checks made on the model during painting.
- Save record: for each image, how it was saved (UI Save, UI Save As, or image.save() in a script), the path written, and the post-save audit line showing dirty=False for saved images.
- Limits: anything not covered, stated plainly (for example a stroke that was not tested interactively, an image left dirty on purpose, or a headless observation such as the texture_paint_images result that was not checked in the UI).
Operating Rules
- No painting before UVs and a wired image slot both check out in an audit. Brush feedback in the viewport is not proof that paint is landing in the right image.
- Color space follows map type: sRGB for color maps, Non-Color for data maps such as roughness, normal, and mask maps. A wrong color space is fixed before painting, not after.
- Paint slots are read from ShaderNodeTexImage nodes in this skill's audit, because Material.texture_paint_images returned an empty list in headless Blender 5.1.1 runs even with a TexImage node present and active. State that limit wherever the slot method matters.
- ImagePaint.brush is read-only in Blender 5.1.1. No step in a script assigns the brush through it.
- The image is saved on its own, separate from saving the .blend file. The dirty flag from the audit is the check: dirty=True means unsaved image changes, dirty=False after a save means that image's changes were written.
- In headless scripts, save with the image.save() method, not bpy.ops.image.save_as, which failed its poll in the headless factory context during verification. In the UI, use the Image menu Save or Save As actions.
- Every number in a deliverable comes from the audit output, the image and material settings read in the session, or a file check on the saved image path. Resolutions, counts, and dirty states remembered from an earlier session are unverified until one of those checks produces them.
- Use the audit script below for the mechanical checks. Save it as paint_audit.py, run it inside Blender as its header describes, and put its full output in the deliverable as item 3 of the Output Contract. Report numbers from its output, not from memory of the scene.
Paint audit (paint_audit.py)
Run: blender -b --factory-startup -noaudio --python paint_audit.py -- --make-fixture builds the worked fixture inside Blender and audits it in the same run, reproducing the output below. Run it on a real file with blender -b your_file.blend -noaudio --python paint_audit.py. The script runs inside Blender, not with system python.
Expected output for the fixture run:
text
blender_version: 5.1.1
paint_objects: 2
object: DataCube
uv_layers: UVMap
paint_slot: material=DataMat active_slot=0 image=Cube_Roughness size=128x128 colorspace=Non-Color dirty=False filepath=/tmp/paint_audit_fixture_roughness.png
object: PaintCube
uv_layers: UVMap
paint_slot: material=PaintMat active_slot=0 image=Cube_BaseColor size=256x256 colorspace=sRGB dirty=True filepath=-
paint_slots: 2
images: 2
images_dirty: 1
images_saved: 1
The fixture is built to show both save states in one run: PaintCube holds Cube_BaseColor at 256x256 in sRGB, edited in memory and never saved, so it reports dirty=True and no filepath. DataCube holds Cube_Roughness at 128x128 in Non-Color, saved to a PNG file by the fixture, so it reports dirty=False with that filepath. That gives paint_objects: 2, paint_slots: 2, images: 2, images_dirty: 1, and images_saved: 1. A default factory scene with no paint setup reports paint_objects: 0, paint_slots: 0, and images: 0.
paint_audit.py (runs inside Blender, using bpy from that session). Save this code as paint_audit.py:
python
#!/usr/bin/env python3
"""Audit texture paint setup in the current Blender scene. Blender 5.1.1.
Run inside Blender, not with system python:
blender -b --factory-startup -noaudio --python paint_audit.py
blender -b your_file.blend -noaudio --python paint_audit.py
Reproduce the worked example in the skill (builds a fixture first):
blender -b --factory-startup -noaudio --python paint_audit.py -- --make-fixture
What it reports: every object whose materials contain an image texture
node (a paint slot candidate), the slot's image name, size, color space,
file path, and dirty flag, plus scene totals. Paint slots are derived
from ShaderNodeTexImage nodes because Material.texture_paint_images
returned an empty list in headless 5.1.1 runs during drafting, even
with a TexImage node present and active.
"""
import os
import sys
import tempfile
import bpy
def make_fixture():
bpy.ops.object.select_all(action="SELECT")
bpy.ops.object.delete()
# PaintCube: base color image, edited in memory, never saved.
bpy.ops.mesh.primitive_cube_add(size=2.0)
paint_obj = bpy.context.active_object
paint_obj.name = "PaintCube"
color_img = bpy.data.images.new("Cube_BaseColor", width=256, height=256, alpha=True)
color_img.colorspace_settings.name = "sRGB"
px = list(color_img.pixels)
px[0] = 0.25
color_img.pixels = px
paint_mat = bpy.data.materials.new("PaintMat")
paint_mat.use_nodes = True
tex = paint_mat.node_tree.nodes.new("ShaderNodeTexImage")
tex.image = color_img
princ = [n for n in paint_mat.node_tree.nodes if n.bl_idname == "ShaderNodeBsdfPrincipled"][0]
paint_mat.node_tree.links.new(tex.outputs["Color"], princ.inputs["Base Color"])
paint_obj.data.materials.append(paint_mat)
# DataCube: data image, saved to a file, so it is not dirty.
bpy.ops.mesh.primitive_cube_add(size=1.0, location=(3.0, 0.0, 0.0))
data_obj = bpy.context.active_object
data_obj.name = "DataCube"
data_img = bpy.data.images.new("Cube_Roughness", width=128, height=128)
data_img.colorspace_settings.name = "Non-Color"
saved_path = os.path.join(tempfile.gettempdir(), "paint_audit_fixture_roughness.png")
data_img.filepath_raw = saved_path
data_img.file_format = "PNG"
px = list(data_img.pixels)
px[0] = 0.5
data_img.pixels = px
data_img.save()
data_mat = bpy.data.materials.new("DataMat")
data_mat.use_nodes = True
tex2 = data_mat.node_tree.nodes.new("ShaderNodeTexImage")
tex2.image = data_img
princ2 = [n for n in data_mat.node_tree.nodes if n.bl_idname == "ShaderNodeBsdfPrincipled"][0]
data_mat.node_tree.links.new(tex2.outputs["Color"], princ2.inputs["Roughness"])
data_obj.data.materials.append(data_mat)
return [paint_obj, data_obj]
def audit():
print(f"blender_version: {bpy.app.version_string}")
rows = []
images = {}
for obj in sorted(bpy.data.objects, key=lambda o: o.name):
if obj.type != "MESH":
continue
slots = []
for mat in obj.data.materials:
if mat is None or not getattr(mat, "node_tree", None):
continue
for node in mat.node_tree.nodes:
if node.bl_idname == "ShaderNodeTexImage" and node.image is not None:
slots.append((mat.name, mat.paint_active_slot, node.image))
images[node.image.name] = node.image
if slots:
rows.append((obj, slots))
print(f"paint_objects: {len(rows)}")
total_slots = 0
for obj, slots in rows:
uv_names = [layer.name for layer in obj.data.uv_layers] if obj.data.uv_layers else []
print(f"object: {obj.name}")
print(f" uv_layers: {', '.join(uv_names) if uv_names else '-'}")
for mat_name, active_slot, img in slots:
total_slots += 1
w, h = img.size[0], img.size[1]
filepath = img.filepath if img.filepath else "-"
print(f" paint_slot: material={mat_name} active_slot={active_slot} image={img.name} size={w}x{h} colorspace={img.colorspace_settings.name} dirty={img.is_dirty} filepath={filepath}")
print(f"paint_slots: {total_slots}")
print(f"images: {len(images)}")
dirty = sum(1 for i in images.values() if i.is_dirty)
print(f"images_dirty: {dirty}")
print(f"images_saved: {len(images) - dirty}")
def main():
args = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
if "--make-fixture" in args:
make_fixture()
audit()
return 0
if __name__ == "__main__":
raise SystemExit(main())
No ratings yet — be the first to rate it.