How do I programmatically set hair position and shape in Blender 2.8? I’m working on a project where I set a (relatively) small number of hair strands using particle system. In Blender 2.79 I’d just loop through obj.particle_systems[0].particles and set each particle’s location and hair_keys[].co. As discussed in https://developer.blender.org/T58792 this doesn’t work now in Blender 2.8, but there seems to be a workaround? (or new intended behaviour?)

Anyway, I don’t get it to work. The hair strands are not updated to the positions I try to set anymore.

So my question is: In Blender 2.8, what is currently the best way to set the hair strands’ position and shape programmatically?



I’ve made a minimal example that tries to make a row of 5 hair strands. See this comparison of the code run in Blender 2.79 and Blender 2.80, respectively:


Here is the Blender 2.79 code that gives the desired result:

import bpy

# 1. Use initial cube as object and set CYCLES
obj = bpy.data.objects['Cube']
bpy.context.scene.render.engine = 'CYCLES'

# 2. Add a particle system
psys = obj.modifiers.new("hair", 'PARTICLE_SYSTEM').particle_system
psys.settings.type = 'HAIR'
psys.settings.use_render_emitter = False
psys.settings.use_strand_primitive = True
psys.settings.cycles.radius_scale = 0.2
psys.settings.hair_step = 3
psys.settings.count = 5

# Connect and disconnect (needed, but why?)
bpy.ops.particle.disconnect_hair(all=True)
bpy.ops.particle.connect_hair(all=True)

# 3. Set hair positions vertices
for m in range(len(psys.particles)):
    psys.particles[m].location = (m, 0, 0)

    for n in range(len(psys.particles[m].hair_keys)):
        psys.particles[m].hair_keys[n].co = (m, 0, n)

# Toggle back and forth to update viewport
bpy.ops.particle.particle_edit_toggle()
bpy.ops.particle.particle_edit_toggle()


And here is my current best attempt at the corresponding Blender 2.80 code (with some minor API-changes + the aforementioned workaround):

import bpy

# 1. Use initial cube as object and set CYCLES
obj = bpy.data.objects['Cube']
bpy.context.scene.render.engine = 'CYCLES'

# 2. Add a particle system
psys = obj.modifiers.new("hair", 'PARTICLE_SYSTEM').particle_system
psys.settings.type = 'HAIR'
obj.show_instancer_for_render = False
psys.settings.use_strand_primitive = True
psys.settings.radius_scale = 0.2
psys.settings.hair_step = 3
psys.settings.count = 5

# Connect and disconnect (needed, but why?)
bpy.ops.particle.disconnect_hair(all=True)
bpy.ops.particle.connect_hair(all=True)

""" https://developer.blender.org/T58792 """
#https://docs.blender.org/api/blender2.8/bpy.types.Depsgraph.html  &lt;--- relevant?
depsgraph = bpy.context.evaluated_depsgraph_get()
object_eval = obj.evaluated_get(depsgraph)
psys_eval = object_eval.particle_systems[0]

print('Number of particles:\n  %d  (psys)\n  %d  (psys_eval)' % (len(psys.particles), len(psys_eval.particles)))
psys = psys_eval
""" ------------------------------------------------------- """

# 3. Set hair positions vertices
for m in range(len(psys.particles)):
    psys.particles[m].location = (m, 0, 0)

    for n in range(len(psys.particles[m].hair_keys)):
        psys.particles[m].hair_keys[n].co = (m, 0, n)

# Toggle back and forth to update viewport
bpy.ops.particle.particle_edit_toggle()
bpy.ops.particle.particle_edit_toggle()


Note: I posted this exact same question on BlenderArtist.org, but afterwards read that StackExchange may be more suited for this type of question so I'll attempt here as well.
Halfway there now…

Following this answer for manipulating particles, and shoehorning this problem with the same approach led to the following:




Viewport: OK! :D
Render preview: OK! :D
Final render: Not OK... :(


Code:

import bpy

# 1. Use initial cube as object and set CYCLES
bpy.context.scene.render.engine = 'CYCLES'
obj = bpy.data.objects["Cube"]

# 2. Add a particle system
psys = obj.modifiers.new("hair", 'PARTICLE_SYSTEM').particle_system
psys.settings.type = 'HAIR'
obj.show_instancer_for_render = False
obj.show_instancer_for_viewport = False
psys.settings.use_strand_primitive = True
psys.settings.radius_scale = 0.2
psys.settings.hair_step = 3
psys.settings.count = 5

# 2½. Animation options
psys.settings.frame_start = 1
psys.settings.frame_end = 1
psys.settings.lifetime = 1000

# 3. Setup frame_change_post-callback
depsgraph = bpy.context.evaluated_depsgraph_get()           # Get depsgraph

# Define particle setter. Set at every frame update(?)
def particleSetter(self):
    particle_systems = obj.evaluated_get(depsgraph).particle_systems

    for m, particle in enumerate(particle_systems[0].particles):
        setattr(particle, "location", (m, 0, 0))
        for n in range(len(particle.hair_keys)):
            setattr(particle.hair_keys[n], "co", (m, 0, n))

    # Toggle back and forth to update viewport
    bpy.ops.particle.particle_edit_toggle()
    bpy.ops.particle.particle_edit_toggle()

# Clear the post frame handler
bpy.app.handlers.frame_change_post.clear()

# Register our particleSetter with the post frame handler
bpy.app.handlers.frame_change_post.append(particleSetter)

# Trigger frame update
bpy.context.scene.frame_current = 0