# Convert particles to animated mesh including existing armature animation

The situation

I am using a boid physics particle system in Blender 2.82. I am rendering the particles using an object that has armature animation. Following a process similar to what is described in this tutorial. This works fine within Blender.

The problem

I would like to export the generated 'flock' to a model (glb to be specific) and in order to do so I need to create real bits of geometry for each particle and copy over the animation per frame of each particles to the newly created geometry.

I saw the answer and script here which works perfecty in terms of copying static geometries, but it does not allow for creating copies of models that have armature included (unless I am mistaken?)

So I adapted the script to the following

import bpy

# Set these to False if you don't want to key that property.
KEYFRAME_LOCATION = True
KEYFRAME_ROTATION = True
KEYFRAME_SCALE = False
MATCHPARTICLE_SCALE = True
KEYFRAME_VISIBILITY = False  # Viewport and render visibility. set this to false otherwise animations don't export to gltf

def create_objects_for_particles(ps, ps_name):

#deselect source
bpy.data.objects[ps_name].select_set(False)

# store a ref for every object to match every particle
obj_list = []

for i, _ in enumerate(ps.particles):

#need to rename duplicate bones for gltf export
boneName = "bone_{}".format(i)

#duplicate objects and store the armature to animate
dupli = bpy.ops.object.duplicate()

#select the recent duplicate objects
activeObjects = bpy.context.selected_objects

#loop through the objects
for ob in activeObjects:

#just get the armature
if ob.type == 'ARMATURE':

#rename bones
for bone in ob.data.bones:
bone.name = boneName

#add the new object to the list
obj_list.append(ob)

return obj_list

def match_and_keyframe_objects(ps, obj_list, start_frame, end_frame):
# Match and keyframe the objects to the particles for every frame in the
# given range.
for frame in range(start_frame, end_frame + 1):
print("frame {} processed".format(frame))
bpy.context.scene.frame_set(frame)
for p, obj in zip(ps.particles, obj_list):
match_object_to_particle(p, obj)
keyframe_obj(obj)

def match_object_to_particle(p, obj):
# Match the location, rotation, scale and visibility of the object to
# the particle.
loc = p.location
rot = p.rotation
size = p.size
if p.alive_state == 'ALIVE':
vis = True
else:
vis = False
obj.location = loc
# Set rotation mode to quaternion to match particle rotation.
obj.rotation_mode = 'QUATERNION'
obj.rotation_quaternion = rot
if MATCHPARTICLE_SCALE:
obj.scale = (size, size, size)
#obj.hide_viewport = not(vis) # <<<-- this was called "hide" in <= 2.79
#obj.hide_render = not(vis)

def keyframe_obj(obj):
# Keyframe location, rotation, scale and visibility if specified.
if KEYFRAME_LOCATION:
obj.keyframe_insert("location")
if KEYFRAME_ROTATION:
obj.keyframe_insert("rotation_quaternion")
if KEYFRAME_SCALE:
obj.keyframe_insert("scale")
if KEYFRAME_VISIBILITY:
obj.keyframe_insert("hide_viewport") # <<<-- this was called "hide" in <= 2.79
obj.keyframe_insert("hide_render")

def main():
#in 2.8 you need to evaluate the Dependency graph in order to get data from animation, modifiers, etc
depsgraph = bpy.context.evaluated_depsgraph_get()

# Assume only 2 objects are selected.
# The active object should be the one with the particle system.
ps_obj = bpy.context.object
ps_obj_evaluated = depsgraph.objects[ ps_obj.name ]
ps_name = ps_obj.name

ps = ps_obj_evaluated.particle_systems[0]  # Assume only 1 particle system is present.
start_frame = bpy.context.scene.frame_start
end_frame = bpy.context.scene.frame_end
obj_list = create_objects_for_particles(ps, ps_name)

match_and_keyframe_objects(ps, obj_list, start_frame, end_frame)

if __name__ == '__main__':
main()


Which works... ish, the problem is that it is very inconsistent.

• Sometimes it works perfectly,

• sometimes it only duplicates the objects but does not produce any keyframes,

• sometimes it just crashes blender altogether. etc. I can't see any logic to when it does or doesn't work.

I think my script is logical and since it does work sometimes I think it must be going in right direction but I am a bit of a novice when it comes to scripting so I may be making basic errors.

I wonder if anyone would be willing to have a go with that script and see if it works for them?

The instructions are similar to the original,

• first you need to select your armatured object (both the mesh and the bones)

• then you select the object that has the particle system attached to it

so you should have 3 objects selected in total. Then run that script.

See below a link to example file, you will find an object with animated armature, a particle system set up and the script is in the scripting tab called 'convert'.

https://cdn.glitch.com/50a342dd-f2eb-4ff5-81ed-e7dff1e4e4cc%2Fparticletogeom_example.blend.zip

Any advice is much appreciated, there might just be some basic issues with my python, too many loops or something.

• Often a good idea to provide a blend file with test scene setup (and script inside to run). blend-exchange.giantcowfilms.com Feb 19, 2020 at 17:23
• thanks for the tip @batFINGER I have added a link to a zipped example file
– Nick
Feb 19, 2020 at 18:08
• wonder if depsgraph needs to be evaluated for each frame? Feb 19, 2020 at 18:42
• Another, perhaps simpler, approach that would work if the animation is simple enough would be to convert the poses to shape keys (blender.stackexchange.com/questions/54174/…) and make small modifications to the original script to just copy the shape keys on the particle instances. Feb 20, 2020 at 9:15
• hi @daylanKifky thanks for your response, I know what you mean and I had tried an early version converting to shapeKeys and I think ultimately that might be a better approach, unfortunately (and not that I specified this in the question) my target environment does not currently support shapekeys as animation so I need to find a solution for bones.
– Nick
Feb 20, 2020 at 9:21

So I kept playing around with this and it turns out that my script was basically fine, but trying to rename the bones during the initial duplication of objects was what was causing the problems/crashes etc. So I've broken that out into a separate function - that particular bit is specific to glb export anyway so I also added a flag to run that or not.

Anyway, posting my working script below for anyone interested.

import bpy

# Set these to False if you don't want to key that property.
KEYFRAME_LOCATION = True
KEYFRAME_ROTATION = True
KEYFRAME_SCALE = False
MATCHPARTICLE_SCALE = True
KEYFRAME_VISIBILITY = False  # Viewport and render visibility. set this to false otherwise animations don't export to gltf
RENAME_BONES = True

def create_objects_for_particles(ps, ps_name, particles_coll):

#deselect particle source
bpy.data.objects[ps_name].select_set(False)

# store a ref for every object to match every particle
obj_list = []

#loop through particles to make a copy for each
for i, _ in enumerate(ps.particles):

#duplicate objects
dupli = bpy.ops.object.duplicate()

#select the recent duplicate objects
activeObjects = bpy.context.selected_objects

#loop through the objects
for ob in activeObjects:

#get current parent collection
currColl = ob.users_collection[0]

#pull out the armature to add to obj_list for animation
if ob.type == 'ARMATURE':

#add the new object to the list
obj_list.append(ob)

return obj_list

def match_and_keyframe_objects(ps, obj_list, start_frame, end_frame):
# Match and keyframe the objects to the particles for every frame in the
# given range.
for frame in range(start_frame, end_frame + 1):
print("frame {} processed".format(frame))
bpy.context.scene.frame_set(frame)
for p, obj in zip(ps.particles, obj_list):
match_object_to_particle(p, obj)
keyframe_obj(obj)

def match_object_to_particle(p, obj):
# Match the location, rotation, scale and visibility of the object to
# the particle.
loc = p.location
rot = p.rotation
size = p.size
if p.alive_state == 'ALIVE':
vis = True
else:
vis = False
obj.location = loc
# Set rotation mode to quaternion to match particle rotation.
obj.rotation_mode = 'QUATERNION'
obj.rotation_quaternion = rot
if MATCHPARTICLE_SCALE:
obj.scale = (size, size, size)
#obj.hide_viewport = not(vis) # <<<-- this was called "hide" in <= 2.79
#obj.hide_render = not(vis)

def keyframe_obj(obj):
# Keyframe location, rotation, scale and visibility if specified.
if KEYFRAME_LOCATION:
obj.keyframe_insert("location")
if KEYFRAME_ROTATION:
obj.keyframe_insert("rotation_quaternion")
if KEYFRAME_SCALE:
obj.keyframe_insert("scale")
if KEYFRAME_VISIBILITY:
obj.keyframe_insert("hide_viewport") # <<<-- this was called "hide" in <= 2.79
obj.keyframe_insert("hide_render")

def rename_bones(particles_coll):

#create count for naming bones
count = 0

#get the new copies collection
coll = bpy.data.collections[particles_coll.name]

#loop through collection
for obj in coll.objects:

#make a new name for each bone
boneName = "bone_{}".format(count)

#loop through bones in armature and rename
if obj.type == 'ARMATURE':

for bone in obj.data.bones:
bone.name = boneName

count = count+1

def main():

#set frame to 0 in case it isn't
bpy.context.scene.frame_set(0)

#in 2.8 you need to evaluate the Dependency graph in order to get data from animation, modifiers, etc
depsgraph = bpy.context.evaluated_depsgraph_get()

# The last object should be the one with the particle system.
ps_obj = bpy.context.object
ps_obj_evaluated = depsgraph.objects[ ps_obj.name ]
ps_name = ps_obj.name

ps = ps_obj_evaluated.particle_systems[0]  # Assume only 1 particle system is present.
start_frame = bpy.context.scene.frame_start
end_frame = bpy.context.scene.frame_end

#create a new empty collection to store duplicates
#do here so can pass to rename_bones
particles_coll = bpy.data.collections.new(name="particle_copies")
$$$$
`