I'm using Blender 3.6 (but I don't think there's a difference if you use 4.0) and am trying to add hair curves onto the surface of a mesh. I'm able to do this via the GUI, however I'm unable to reproduce the same result via a python script using bpy commands. I'm going to first explain how I go about doing this using the GUI and then explain some steps I've taken to reproduce this via a python script and where I'm stuck.

GUI based hair attachment

  • Open a new blend file using Blender 3.6
  • Add > Mesh > Icosphere (this is the geometry we want to attach hair to)
  • With the Icosphere selected, Add > Curve > Empty Hair (this will create a Curves object which will be child of the Icosphere)
  • Now you can draw the hair:
    • Select the Curves object and go into sculpt mode
    • Select the Add brush (which looks like two waves with a plus sign)
    • Click on screen where you want to add the curves.

If all goes well you should seem something like this image (here I clicked on the screen 3 times so you see 3 total curves) enter image description here

Python based hair attachment

import bpy

def draw_arbitrary_hair():
    # Clear Screen
    for obj in bpy.data.objects:
        bpy.data.objects.remove(obj, do_unlink=True)

    # Create basic ico_sphere with curves object as child

    # We need to be in VIEW_3D area to have to find builtin_brush.Add 
    with bpy.context.temp_override(**context_override("VIEW_3D")):
        # Move into sculpt mode and select brush

        # Define example stroke
        strokes = [{    
            "name": "stroke",
            "mouse": (0, 0),
            "pen_flip": False,
            "is_start": True,
            "location": (0.95106, -0.3091, 0), # location of a vertex I choose, but could idealy be any location on/near surface
            "size": 1.0,
            "pressure": 1.0, 
            "time": 0,
            "mouse_event": (0,0),

        # Apply stroke
        bpy.ops.sculpt_curves.brush_stroke(stroke=strokes, mode="NORMAL")

def context_override(desired_context):
    override_context = bpy.context.copy()
    area = [area for area in bpy.context.screen.areas if area.type == desired_context][0]
    override_context['window'] = bpy.context.window
    override_context['screen'] = bpy.context.screen
    override_context['area'] = area
    override_context['region'] = area.regions[-1]
    override_context['scene'] = bpy.context.scene
    override_context['space_data'] = area.spaces.active
    return override_context


However the above code does not create any hair strands. It feels like no operation occurred. The code doesn't throw an exception inside of Blender's python info tab (it just shows as: bpy.ops.text.run_script() with no error). However if you got to Window > Toggle System Console then you will see that it does indeed throw an error.


ERROR (wm.operator): C:\Users\blender\git\blender-v360\blender.git\source\blender\windowmanager\intern\wm_event_system.cc:1551 wm_operator_invoke: invalid operator call 'SCULPT_CURVES_OT_brush_stroke'

If I try to type these commands into the python console one at a time I get {PASS_THROUGH} as the result of the bpy.ops.sculpt_curves.brush_stroke(stroke=strokes, mode="NORMAL") call, so indeed nothing happened.

If anyone has any advice on what could be causing this issue, I would really appreciate the help.


I've been using this post for looking how to use brush via the python api

I also thought it was possible I was overriding the context incorrectly so I tried this approach too, but it didn't help:

Lastly, I also tried to create a specific brush solely for the purposes of adding specific hair strands by adding the following before defining the stroke:

# Create a brush capable of adding hair strands
bpy.data.brushes.new("ADD_HAIR_BRUSH", mode="SCULPT_CURVES")
bpy.data.brushes["ADD_HAIR_BRUSH"].curves_sculpt_tool = 'ADD'

# Move into sculpt mode and select brush
bpy.ops.paint.brush_select(curves_sculpt_tool='ADD', toggle=False)
  • $\begingroup$ Thank you @Blunder, I've updated my original post to add your fixes. $\endgroup$ Commented Jan 25 at 18:55
  • $\begingroup$ Hi. Thanks for the update. According to the documentation of bpy.ops.sculpt_curves the strokes should be a bpy_prop_collection of OperatorStrokeElement. In your code it's a list with a dict in it. Maybe this is the reason why the operator fails. -- Alternatively, you could try to create the curves with the API directly (e.g. docs.blender.org/api/current/bpy.types.BlendDataHairCurves.html). $\endgroup$
    – Blunder
    Commented Jan 26 at 11:38
  • $\begingroup$ @Blunder I'm sorry but would you be able to help me figure out how to create a "bpy_prop_collection of OperatorStrokeElement"? I've tried the following: bpy.types.bpy_prop_collection(bpy.types.OperatorStrokeElement()) but got this error: TypeError: bpy_struct.__new__(type): expected a single argument. Looking at the documentation I'm confused how to create a PropertyGroup object that can be passed into the OperatorStrokeElement function. Where do I define all of the class variables? $\endgroup$ Commented Jan 26 at 21:47
  • $\begingroup$ I also tried something like: class MyCustomOperatorStrokeElement(bpy.types.OperatorStrokeElement): def register(): bpy.utils.register_class(MyCustomOperatorStrokeElement) def unregister(): bpy.utils.unregister_class(MyCustomOperatorStrokeElement) But I'm not sure how to actually use my new custom operator? $\endgroup$ Commented Jan 26 at 21:50


You must log in to answer this question.

Browse other questions tagged .