[AddOn] Import OBJ Sequence As Shape Keys(on one object)

This script allows an OBJ sequence to be imported as a shape animation. The imported result is one object with animated shape keys. This script automates the process used to make animations like this one by Ian McGlasham:

There is a tutorial on how to make an animation like the one above here.

Initial setup is as follows:

  1. Select “File” - “Import” - “Wavefront.obj” from the “file” menu and navigate to the directory which holds the OBJ sequence
  2. Select the first OBJ file in the sequence and click “Import OBJ”.
  3. Select the newly imported object in the 3D view or the outliner
  4. Select the “Object Data” tab in the main properties panel
  5. In the “Shape keys” section, click the “Add shape key to object” button (+)
  6. Uncheck the “Relative” Checkbox and set the interpolation dropdown menu to “Linear”
  7. Change the current frame in the timeline to “0” and set a keyframe for “Evaluation time” (as “0”)
  8. Change the current frame on the timeline to “1000”
  9. Enter “10000” as evaluation time. (it seems to go in steps of 10 strangely!), then set a keyframe for “Evaluation time”
  10. Change to the graph editor window, then select the curve for “Evaluation time”
  11. Select “Key” - “Interpolation Mode” - “Linear” from the graph editor menu.
  12. Change back to the 3D view.

Once you done that, you use the script to import the sequence:

  1. Select “File” - “Import” - “OBJ as Shapekey” from the “file” menu and navigate to the directory which holds the OBJ sequence.
  2. Select the rest of the sequence(all except for the first obj file in the sequence) and click “Import OBJ as Shape Keys”.
  3. Wait as it loads the sequence…

io_obj_as_shapekey.zip (1.67 KB)
io_obj_as_shapekey_macosx.zip (1.68 KB)

Ian McGlasham has created a tutorial showing the process of creating an animation using the script. See link in original post.

coool!
thanks man, test soon :smiley:

Greetings!

Blender noob here attempting to re-create the animation in Ian McGlasham’s excellent tutorial. Per Mr. McGlasham’s instructions, I imported obj as shapekeys using your script - but I’ve come across some unusual results. My cube animation exported to obj is 420 frames, but upon import as shapekeys, 33 of the 419 .objs are imported as mesh objects (see attached pic). Ive tried this in both 2.67 and 2.72b with the same result. Any thoughts as to why this might be happening?


I’m about as new to blender as it gets, so I’m probably missing something obvious. Any help greatly appreciated.

Regards,

S. M. Blanchard

Works great, many thanks! :eyebrowlift:

When I do the import step, I’m getting each obj as a new mesh and not as a shape key, although unlike SMTB none of my imports become shape keys.

Has anyone else had this problem?

Many thanks!

Initially when I tried to use this script I got the following error

/uploads/default/original/4X/a/7/f/a7fd7b201c71f2b1a154a26cdfa7f0a49d05bed6.jpgstc=1

I fixed this by changing line 55 from

        fp = spath[0] + "\\" + f

to

        fp = spath[0] + "//" + f

I’m using Blender 2.77 on Ubuntu 16.04

Attachments


@hellocatfood Same here on Linux Mint 17.3 and I guess it goes the same for the rest of the Linux users. Change that line and everything should be OK!

fp =os.path.join(spath[0], f)

The imports all load bbut no shape keys–on Mac -here is my error message–I really appreciate any help! G.E.

(line 59):

sknames = [sk.name for sk in target.data.shape_keys]
TypeError: ‘NoneType’ object is not iterable

would this work on multiple objects if you were to make another operator that executes the current one(hide current from layout), but instead of context.active, use context.selected_objects?

thanks for such quick consideration–i don’t know Python so I cant rewrite a function for the array–any help much appreciated
here is the script

##### BEGIN GPL LICENSE BLOCK

This program is free software; you can redistribute it and/or

modify it under the terms of the GNU General Public License

as published by the Free Software Foundation; either version 2

of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,

but WITHOUT ANY WARRANTY; without even the implied warranty of

MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

GNU General Public License for more details.

You should have received a copy of the GNU General Public License

along with this program; if not, write to the Free Software Foundation,

Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

##### END GPL LICENSE BLOCK

bl_info = {
‘name’: ‘Load Obj Sequence as Shape Keys’,
‘author’: ‘cmomoney’,
‘version’: (0, 2),
‘blender’: (2, 6, 7),
‘category’: ‘Import-Export’,
‘location’: ‘File > Import/Export’,
‘wiki_url’: ‘’}

import bpy
import os
from bpy.props import *

class LoadObjAsShapekey(bpy.types.Operator):
bl_idname = ‘load.obj_as_shapekey’
bl_label = ‘Import OBJ as Shape Keys’
bl_options = {‘REGISTER’, ‘UNDO’}
bl_description = “Import Obj sequence as shape key(s)”

filepath = StringProperty(name="File path", description="File filepath of Obj", maxlen=4096, default="")
filter_folder = BoolProperty(name="Filter folders", description="", default=True, options={'HIDDEN'})
filter_glob = StringProperty(default="*.obj", options={'HIDDEN'})
files = CollectionProperty(name='File path', type=bpy.types.OperatorFileListElement)
filename_ext = '.obj'


@classmethod
def poll(cls, context):
    return context.active_object is not None and context.active_object.type == 'MESH'


def execute(self, context):
    # get file names, sort, and set target mesh
    spath = os.path.split(self.filepath)
    files = sorted([file.name for file in self.files])
    target = bpy.context.scene.objects.active
    # add all ojs in sequence as shape  keys
    for f in files:
        fp = spath[0] + "\" + f
        self.load_obj(fp)
    # now delete objs

sknames = [sk.name for sk in target.data.shape_keys.key_blocks]
bpy.ops.object.select_all(action=‘DESELECT’)
for obj in sknames:
if obj != ‘Basis’:
target.data.shape_keys.key_blocks[obj].interpolation = ‘KEY_LINEAR’
bpy.context.scene.objects.active = bpy.data.objects[obj]
bpy.data.objects[obj].select = True
bpy.ops.object.delete()
bpy.ops.object.select_all(action=‘DESELECT’)
# reselect target mesh and make active
bpy.context.scene.objects.active = target
target.select = True
return{‘FINISHED’}

def invoke(self, context, event):
    wm = context.window_manager.fileselect_add(self)
    return {'RUNNING_MODAL'}


def load_obj(self, fp):
    bpy.ops.import_scene.obj(filepath=fp)
    bpy.ops.object.join_shapes()
    return

def menu_func_import(self, context):
self.layout.operator(LoadObjAsShapekey.bl_idname, text=“Obj As Shapekey(.obj)”)

def register():
bpy.utils.register_class(LoadObjAsShapekey)
bpy.types.INFO_MT_file_import.append(menu_func_import)

def unregister():
bpy.utils.unregister_class(LoadObjAsShapekey)
bpy.types.INFO_MT_file_import.remove(menu_func_import)

if name == “main”:
register()

@cmomoney
I tried the script with simple obj seq and everything was fine
but with highpoly (realflow) it’s not working
that’s what I got


I added the file name to shape name functionality:



bl_info = {
    'name': 'Load Obj Sequence as Shape Keys',
    'author': 'cmomoney',
    'version': (0, 2),
    'blender': (2, 6, 7),
    'category': 'Import-Export',
    'location': 'File > Import/Export',
    'wiki_url': ''}


# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####


import bpy, os
from bpy.props import *


class LoadObjAsShapekey(bpy.types.Operator):
    bl_idname = 'load.obj_as_shapekey'
    bl_label = 'Import OBJ as Shape Keys'
    bl_options = {'REGISTER', 'UNDO'}
    bl_description = "Import Obj sequence as shape key(s)"


    filepath = StringProperty(name="File path", description="File filepath of Obj", maxlen=4096, default="")
    filter_folder = BoolProperty(name="Filter folders", description="", default=True, options={'HIDDEN'})
    filter_glob = StringProperty(default="*.obj", options={'HIDDEN'})
    files = CollectionProperty(name='File path', type=bpy.types.OperatorFileListElement)
    filename_ext = '.obj'
	
    @classmethod


    def poll(cls, context):
        return context.active_object is not None and context.active_object.type == 'MESH'


    def execute(self, context):
        #get file names, sort, and set target mesh
        spath = os.path.split(self.filepath)
        files = [file.name for file in self.files]
        files.sort()
        x = 0
        target = bpy.context.scene.objects.active
        #add all ojs in sequence as shape  keys
        for f in files:
            fp = spath[0] + "\\" + f
            self.load_obj(fp)
        #now delete objs
        sknames = [sk.name for sk in target.data.shape_keys.key_blocks]
        bpy.ops.object.select_all(action='DESELECT')
        for obj in sknames:
            if obj != 'Basis':
                target.data.shape_keys.key_blocks[obj].interpolation = 'KEY_LINEAR'
				#target.data.shape_keys.key_blocks[obj].name = 'Test'
                bpy.context.scene.objects.active = bpy.data.objects[obj]
                bpy.data.objects[obj].select = True
                bpy.ops.object.delete()
            bpy.ops.object.select_all(action='DESELECT')
        #reselect target mesh and make active
        bpy.context.scene.objects.active = target
        target.select = True
        for obj in sknames:
            if obj != 'Basis':
                target.data.shape_keys.key_blocks[obj].name = os.path.splitext(files[x])[0]
                x+=1
        return{'FINISHED'}


    def invoke(self, context, event):
        wm = context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}


    def load_obj(self, fp):
        bpy.ops.import_scene.obj(filepath=fp)
        bpy.ops.object.join_shapes()
        return


def menu_func_import(self, context):
    self.layout.operator(LoadObjAsShapekey.bl_idname, text="Obj As Shapekey(.obj)")


def register():
    bpy.utils.register_class(LoadObjAsShapekey)
    bpy.types.INFO_MT_file_import.append(menu_func_import)


def unregister():
    bpy.utils.unregister_class(LoadObjAsShapekey)
    bpy.types.INFO_MT_file_import.remove(menu_func_import)


if __name__ == "__main__":
    register()


1 Like

To any expert outhere?? what do i do if i want all the resulting shape keys joined into A SINGLE SHAPE KEY maintaining the order of the animated sequence??
is that possible? is there a script for it???

Thank you, thank you so much for any replies…

If you import multiple shapes into a scene, then join them with Ctrl+J, all of the shapes would be in a single shape key.

If this is your goal, mission accomplished, but I’m not sure what you mean by “maintaining the order of he animated sequence.” Unless you’re using an addon with a specific usecase, the only way to animate the shapes, is by having each mesh having it’s own shapekey.

Ran the script in Blender 2.80? It did not add anything to the import menu. Does this script work with Blender 2.80? If not, what do I change in the script to make it work?

Tried it in 2.79 too. The “Obj As Shapekeys” appears in the import menu, but is greyed out and unusable. Any help in this regard is really appreciated.

Hi,
to get it to work you load in the first obj (i.e. the basis), select it, and then the menu item won’t be greyed out any more. You can then select the remainder of the objects to be converted.

Unfortunately you have to do this in 2.79 and move it to 2.8 as an FBX. Hopefully this post my prompt a python savvy forum member to kindly update the script.

Cheers,
Ian

updated script to work on blender 2.8:

bl_info = {
    'name': 'Load Obj Sequence as Shape Keys',
    'author': 'cmomoney',
    'version': (0, 2),
    'blender': (2, 6, 7),
    'category': 'Import-Export',
    'location': 'File > Import/Export',
    'wiki_url': ''}

# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

import bpy, os
from bpy.props import *

class LoadObjAsShapekey(bpy.types.Operator):
    bl_idname = 'load.obj_as_shapekey'
    bl_label = 'Import OBJ as Shape Keys'
    bl_options = {'REGISTER', 'UNDO'}
    bl_description = "Import Obj sequence as shape key(s)"

    filepath = StringProperty(name="File path", description="File filepath of Obj", maxlen=4096, default="")
    filter_folder = BoolProperty(name="Filter folders", description="", default=True, options={'HIDDEN'})
    filter_glob = StringProperty(default="*.obj", options={'HIDDEN'})
    files = CollectionProperty(name='File path', type=bpy.types.OperatorFileListElement)
    filename_ext = '.obj'

    @classmethod
    def poll(cls, context):
        return context.active_object is not None and context.active_object.type == 'MESH'

    def execute(self, context):
        #get file names, sort, and set target mesh
        spath = os.path.split(self.filepath)
        files = [file.name for file in self.files]
        files.sort()
        target = bpy.context.view_layer.objects.active
        #add all ojs in sequence as shape  keys
        for f in files:
            fp = os.path.join(spath[0], f)
            self.load_obj(fp)
        #now delete objs
        sknames = [sk.name for sk in target.data.shape_keys.key_blocks]
        bpy.ops.object.select_all(action='DESELECT')
        for obj in sknames:
            if obj != 'Basis':
                target.data.shape_keys.key_blocks[obj].interpolation = 'KEY_LINEAR'
                bpy.context.view_layer.objects.active = bpy.data.objects[obj]
                bpy.data.objects[obj].select_set(state=True)
                bpy.ops.object.delete()
            bpy.ops.object.select_all(action='DESELECT')
        #reselect target mesh and make active
        bpy.context.view_layer.objects.active = target
        target.select_set(state=True)
        return{'FINISHED'}

    def invoke(self, context, event):
        wm = context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}

    def load_obj(self, fp):
        bpy.ops.import_scene.obj(filepath=fp,split_mode='OFF')
        bpy.ops.object.join_shapes()
        return

def menu_func_import(self, context):
    self.layout.operator(LoadObjAsShapekey.bl_idname, text="Obj As Shapekey(.obj)")

def register():
    bpy.utils.register_class(LoadObjAsShapekey)
    bpy.types.TOPBAR_MT_file_import.append(menu_func_import)

def unregister():
    bpy.utils.unregister_class(LoadObjAsShapekey)
    bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)

if __name__ == "__main__":
    register()
2 Likes

Hey I’ve written a new version that will automatically do everything for you. You just need to install this addon, then go to import->Obj Seq as ShapeKey, and select all the obj files you want to make into a sequence, and wait a while (2-3 obj/second). You can also see the progress from the console.

bl_info = {
    "name": "Import Obj Sequence",
    "author": "MarcusZ",
    "version": (1, 0, 0),
    "blender": (2, 80, 0),
    "location": "File > Import/Export",
    "description": "Import Obj Sequence",
    "doc_url": "",
    "support": "COMMUNITY",
    "category": "Import-Export",
}


import bpy
from pathlib import Path
from bpy.props import (
    CollectionProperty,
    StringProperty,
)
from bpy_extras.io_utils import ImportHelper


class ImportObjSeq(bpy.types.Operator, ImportHelper):
    """Load an OBJ Sequence as absolute shape keys"""
    
    bl_idname = "import_scene.objseq"
    bl_label = "Import OBJ Seq"
    bl_options = {'REGISTER', 'UNDO'}

    filename_ext = ".obj"
    filter_glob: StringProperty(default="*.obj", options={"HIDDEN"})
    
    files: CollectionProperty(
        name="File Path",
        description="File path used for importing the OBJ sequence",
        type=bpy.types.OperatorFileListElement,
    )
    directory: StringProperty()

    def execute(self, context):
        filepaths = [Path(self.directory, n.name) for n in self.files]
        if not filepaths:
            filepaths.append(self.filepath)
        
        self.create_shapekeys(filepaths)
        
        return{'FINISHED'}

    def create_shapekeys(self, filepaths):
        from contextlib import redirect_stdout
        
        def import_obj(filepath):
            with redirect_stdout(None):
                bpy.ops.import_scene.obj(filepath = filepath, split_mode="OFF")

        # import main obj
        import_obj(str(filepaths[0]))
        main_obj = bpy.context.selected_objects[-1]
        main_obj.rotation_euler = [0,0,0]
        main_obj.shape_key_add(name=main_obj.name)
        main_key = main_obj.data.shape_keys
        bpy.context.view_layer.objects.active = main_obj

        seq_len = len(filepaths)

        # import the rest
        for i, filepath in enumerate(filepaths[1:]):
            import_obj(str(filepath))
            current_obj = bpy.context.selected_objects[-1]
            current_obj.rotation_euler = [0,0,0]
            
            # join as shapes
            bpy.ops.object.join_shapes()
            print(f"{i}/{seq_len}", end='\r')
            
            # remove meshes
            current_mesh = current_obj.data
            bpy.data.objects.remove(current_obj, do_unlink=True)
            bpy.data.meshes.remove(current_mesh, do_unlink=True)

        # set driver
        main_key.use_relative = False
        fcurve = main_key.driver_add('eval_time')
        fcurve.driver.expression = "frame*10"

        # set start/end time
        bpy.context.scene.frame_start = 0
        bpy.context.scene.frame_end = seq_len - 1


def menu_func_import(self, context):
    self.layout.operator(ImportObjSeq.bl_idname, text="Obj Seq As Shapekey(.obj)")

def register():
    bpy.utils.register_class(ImportObjSeq)
    bpy.types.TOPBAR_MT_file_import.append(menu_func_import)

def unregister():
    bpy.utils.unregister_class(ImportObjSeq)
    bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)

if __name__ == "__main__":
    register()

2 Likes