Problem
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)
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)
bpy.ops.outliner.orphans_purge()
# Create basic ico_sphere with curves object as child
bpy.ops.mesh.primitive_ico_sphere_add()
bpy.ops.object.curves_empty_hair_add()
# 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
bpy.ops.object.mode_set(mode='SCULPT_CURVES')
bpy.ops.wm.tool_set_by_id(name="builtin_brush.Add")
# 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),
"x_tilt":0.0,
"y_tilt":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
draw_arbitrary_hair()
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:
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.
References
I've been using this post for looking how to use brush via the python api
- How to sculpt a continuous brush stroke with python?
- however they use a
bpy.ops.sculpt.brush_stroke
while I'm using abpy.ops.sculpt_curves.brush_stroke
operation which doesn't seem to have any examples online.
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.object.mode_set(mode='SCULPT_CURVES')
bpy.ops.paint.brush_select(curves_sculpt_tool='ADD', toggle=False)
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 theOperatorStrokeElement
function. Where do I define all of the class variables? $\endgroup$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$