2
$\begingroup$

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.

$\endgroup$
  • $\begingroup$ Often a good idea to provide a blend file with test scene setup (and script inside to run). blend-exchange.giantcowfilms.com $\endgroup$ – batFINGER Feb 19 at 17:23
  • 1
    $\begingroup$ thanks for the tip @batFINGER I have added a link to a zipped example file $\endgroup$ – Nick Feb 19 at 18:08
  • 1
    $\begingroup$ wonder if depsgraph needs to be evaluated for each frame? $\endgroup$ – lemon Feb 19 at 18:42
  • $\begingroup$ 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. $\endgroup$ – daylanKifky Feb 20 at 9:15
  • $\begingroup$ 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. $\endgroup$ – Nick Feb 20 at 9:21
0
$\begingroup$

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]
            #unlink from that
            currColl.objects.unlink(ob)
            #add to newly created collection
            particles_coll.objects.link(ob)

            #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")
    bpy.context.scene.collection.children.link(particles_coll)

    obj_list = create_objects_for_particles(ps, ps_name, particles_coll)

    match_and_keyframe_objects(ps, obj_list, start_frame, end_frame)   

    if RENAME_BONES:
        rename_bones(particles_coll)

if __name__ == '__main__':
    main()
```
| improve this answer | |
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.