diff --git a/README.md b/README.md
index 3b1d4ea..52de63b 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,9 @@
# Stop-motion-OBJ
A Blender add-on for importing a sequence of meshes as frames
-Stop motion OBJ allows you to import a sequence of OBJ (or STL or PLY) files and render them as individual frames. Have a RealFlow animation but want to render it in Blender? This addon is for you! Currently tested for Blender 2.79.1.
+Stop motion OBJ allows you to import a sequence of OBJ (or STL or PLY) files and render them as individual frames. Have a RealFlow animation but want to render it in Blender? This addon is for you! There are now two versions:
+- [v2.0.0](https://github.com/neverhood311/Stop-motion-OBJ/releases/tag/v2.0.0) for **Blender 2.80+**
+- [r1.1.1](https://github.com/neverhood311/Stop-motion-OBJ/releases/tag/0.2.79.2) for Blender 2.79 (also tested for 2.77 and 2.78). This version is now deprecated and will no longer be supported
If you find this add-on helpful, please consider donating to support development:
@@ -19,6 +21,8 @@ PayPal: https://www.paypal.me/justinj
- Supports shapes with UVs and image textures
- Variable playback speed
- Multiple playback modes
+- Reload from disk
+ - If you change your mesh sequence, you can reload the sequence into the existing object without deleting it and creating a new one
- Object can have different materials on each frame
- For instance, you can have a different MTL file for each OBJ file
- Bake sequence
@@ -38,7 +42,7 @@ PayPal: https://www.paypal.me/justinj
- Sequences can now be duplicated, but they share a material. For a duplicate sequence with a different material, you have to re-import the sequence.
## Installing Stop motion OBJ
-- Download mesh_sequence_controller.py and move it to Blender's addons folder (something like C:\Program Files\Blender Foundation\Blender\2.79\scripts\addons)
+- Download mesh_sequence_controller.py and move it to Blender's addons folder (something like C:\Program Files\Blender Foundation\Blender\2.80\scripts\addons)
- Open Blender and open the Add-ons preferences (File > User Preferences... > Add-ons)
- In the search bar, type 'OBJ' and look for the Stop motion OBJ addon in the list
- Check the box to install it, and click 'Save User Settings'
@@ -50,7 +54,7 @@ PayPal: https://www.paypal.me/justinj
- The object will initially be empty. We need to load a mesh sequence into it.
- Make sure the object is selected.
- In the properties panel, click on the Object Properties tab (the little orange cube icon). In the settings panel, scroll down to find the Mesh Sequence subpanel and open it.
-- Enter the Root Folder by clicking on the folder button and navigating to the folder where the mesh files are stored. **Make sure to UNCHECK ‘Relative Path’**
+- Enter the Root Folder by clicking on the folder button and navigating to the folder where the mesh files are stored. ~~**Make sure to UNCHECK ‘Relative Path’**~~
- In the File Name box, enter a common prefix of the files.
- ex: If you have frame001, frame002, frame003, you could enter ‘frame’, 'fram', or even 'f'
- If your sequence has a different material for each frame, check the "Material per Frame" checkbox. Otherwise, leave it unchecked.
@@ -66,3 +70,6 @@ PayPal: https://www.paypal.me/justinj
- Once your sequence is loaded, you can change the shading (smooth or flat) of the entire sequence:
- The shading buttons are found in the Mesh Sequence Settings for the object
- Note: The normal shading buttons (in the 3D View "Tools" panel) will only affect the current frame, not the entire sequence
+- If you change your mesh sequence and want to reload it without creating a new sequence object, click 'Reload From Disk'
+ - This will use the original Root Folder and File Name that you initially specified
+ - If your updated sequence has more or fewer frames than the original one, the updated sequence object will reflect this change
\ No newline at end of file
diff --git a/mesh_sequence_controller.py b/stop_motion_obj.py
similarity index 76%
rename from mesh_sequence_controller.py
rename to stop_motion_obj.py
index 52ce107..d0534e2 100644
--- a/mesh_sequence_controller.py
+++ b/stop_motion_obj.py
@@ -1,630 +1,714 @@
-# ##### BEGIN GPL LICENSE BLOCK #####
-#
-# Stop motion OBJ: A Mesh sequence importer for Blender
-# Copyright (C) 2016-2018 Justin Jensen
-#
-# 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 3 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, see .
-#
-# ##### END GPL LICENSE BLOCK #####
-
-
-bl_info = {
- "name" : "Stop motion OBJ",
- "description": "Import a sequence of OBJ (or STL or PLY) files and display them each as a single frame of animation. This add-on also supports the .STL and .PLY file formats.",
- "author": "Justin Jensen",
- "version": (0, 2),
- "blender": (2, 79, 0),
- "location": "View 3D > Add > Mesh > Mesh Sequence",
- "warning": "",
- "category": "Add Mesh",
- "wiki_url": "https://github.com/neverhood311/Stop-motion-OBJ",
- "tracker_url": "https://github.com/neverhood311/Stop-motion-OBJ/issues"
-}
-
-import bpy
-import os
-import re
-import glob
-from bpy.app.handlers import persistent
-
-def alphanumKey(string):
- """ Turn a string into a list of string and number chunks.
- "z23a" -> ["z", 23, "a"]
- """
- return [int(c) if c.isdigit() else c for c in re.split('([0-9]+)', string)]
-
-#global variable for the MeshSequenceController
-MSC = None
-
-def deselectAll():
- for ob in bpy.context.scene.objects:
- ob.select = False
-
-#set the frame number for all mesh sequence objects
-#COMMENT THIS persistent OUT WHEN RUNNING FROM THE TEXT EDITOR
-@persistent
-def updateFrame(dummy):
- scn = bpy.context.scene
- global MSC
- MSC.setFrame(scn.frame_current)
-
-#runs every time the start frame of an object is changed
-def updateStartFrame(self, context):
- updateFrame(0)
- return None
-
-class MeshSequenceSettings(bpy.types.PropertyGroup):
- dirPath = bpy.props.StringProperty(
- name="Root Folder",
- description="Only .OBJ files will be listed",
- subtype="DIR_PATH")
- fileName = bpy.props.StringProperty(name='File Name')
- startFrame = bpy.props.IntProperty(
- name='Start Frame',
- update=updateStartFrame,
- default=1)
- #A long list of mesh names
- meshNames = bpy.props.StringProperty()
- numMeshes = bpy.props.IntProperty()
- initialized = bpy.props.BoolProperty(default=False)
- loaded = bpy.props.BoolProperty(default=False)
-
- #out-of-range frame mode
- frameMode = bpy.props.EnumProperty(
- items = [('0', 'Blank', 'Object disappears when frame is out of range'),
- ('1', 'Extend', 'First and last frames are duplicated'),
- ('2', 'Repeat', 'Repeat the animation'),
- ('3', 'Bounce', 'Play in reverse at the end of the frame range')],
- name='Frame Mode',
- default='1')
-
- #material mode (one material total or one material per frame)
- perFrameMaterial = bpy.props.BoolProperty(
- name='Material per Frame',
- default=False
- )
-
- #playback speed
- speed = bpy.props.FloatProperty(
- name='Playback Speed',
- min=0.0001,
- soft_min=0.01,
- step=25,
- precision=2,
- default=1
- )
-
- #the file format for files in the sequence (OBJ, STL, or PLY)
- fileFormat = bpy.props.EnumProperty(
- items = [('0', 'OBJ', 'Wavefront OBJ'),
- ('1', 'STL', 'STereoLithography'),
- ('2', 'PLY', 'Stanford PLY')],
- name='File Format',
- default='0')
-
-class MeshSequenceController:
-
- def __init__(self):
- #for each object in bpy.data.objects:
- for obj in bpy.data.objects:
- #for obj in bpy.context.scene.objects:
- #if it's a sequence object (we'll have to figure out how to indicate this, probably with a T/F custom property)
- if(obj.mesh_sequence_settings.initialized == True):
- #call sequence.loadSequenceFromData() on it
- self.loadSequenceFromData(obj)
- self.freeUnusedMeshes()
-
- def newMeshSequence(self):
- #create an empty mesh
- emptyMesh = bpy.data.meshes.new('emptyMesh')
- #give it a fake user
- emptyMesh.use_fake_user = True
- #make sure it knows it's part of a mesh sequence
- emptyMesh.inMeshSequence = True
- #create a new object containing the empty mesh
- theObj = bpy.data.objects.new("sequence", emptyMesh)
- theObj.mesh_sequence_settings.meshNames = emptyMesh.name + '/'
- #link the object to the scene
- scn = bpy.context.scene
- scn.objects.link(theObj)
-
- #deselect all other objects
- deselectAll()
-
- #select the object
- scn.objects.active = theObj
- theObj.select = True
-
- theObj.mesh_sequence_settings.initialized = True
- return theObj
-
- def loadSequenceFromFile(self, _obj, _dir, _file):
- scn = bpy.context.scene
- #get the file format
- format = 'obj'
- formatidx = int(_obj.mesh_sequence_settings.fileFormat)
- importFunc = None
- #OBJ
- if(formatidx == 0):
- format = 'obj'
- importFunc = bpy.ops.import_scene.obj
- #STL
- elif(formatidx == 1):
- format = 'stl'
- importFunc = bpy.ops.import_mesh.stl
- #PLY
- elif(formatidx == 2):
- format = 'ply'
- importFunc = bpy.ops.import_mesh.ply
- #combine the file directory with the filename and the .obj extension
- full_dirpath = bpy.path.abspath(_dir)
- full_filepath = os.path.join(full_dirpath, _file + '*.' + format)
-
- numFrames = 0
- unsortedFiles = glob.glob(full_filepath)
- # Sort the given list in the way that humans expect.
- sortedFiles = sorted(unsortedFiles, key=alphanumKey)
-
- #for each file that matches the glob query:
- for file in sortedFiles:
- #import the mesh file
- importFunc(filepath = file)
- #get a reference to it
- tmpObject = bpy.context.selected_objects[0]
- tmpMesh = tmpObject.data #don't copy it; just copy the pointer. This cuts memory usage in half.
- tmpMesh.use_fake_user = True
- tmpMesh.inMeshSequence = True
- #deselect all objects
- deselectAll()
- #select the object
- tmpObject.select = True
- #delete it
- bpy.ops.object.delete()
- #add the new mesh's name to the sequence object's text property
- #add the '/' character as a delimiter
- #http://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
- _obj.mesh_sequence_settings.meshNames += tmpMesh.name + '/'
- numFrames+=1
-
- _obj.mesh_sequence_settings.numMeshes = numFrames+1
- if(numFrames > 0):
- #remove the last '/' from the string
- _obj.mesh_sequence_settings.meshNames = _obj.mesh_sequence_settings.meshNames[:-1]
- #set the sequence object's mesh to meshes[1]
- self.setFrameObj(_obj, scn.frame_current)
-
- #select the sequence object
- scn.objects.active = _obj
- _obj.select = True
-
- _obj.mesh_sequence_settings.loaded = True
-
- return numFrames
-
- #this is used when a mesh sequence object has been saved and subsequently found in a .blend file
- def loadSequenceFromData(self, _obj):
- scn = bpy.context.scene
- t_meshNames = _obj.mesh_sequence_settings.meshNames.split('/')
- #count the number of mesh names
- #(helps with backwards compatibility)
- _obj.mesh_sequence_settings.numMeshes = len(t_meshNames)
-
- #make sure the meshes know they're part of a mesh sequence
- #(helps with backwards compatibility)
- for t_meshName in t_meshNames:
- bpy.data.meshes[t_meshName].inMeshSequence = True
-
- #deselect all objects (otherwise everything that is selected will get deleted)
- deselectAll()
-
- #select the sequence object
- scn.objects.active = _obj
- _obj.select = True
- #set the frame number
- self.setFrameObj(_obj, scn.frame_current)
-
- _obj.mesh_sequence_settings.loaded = True
-
- def getMesh(self, _obj, _idx):
- #get the object's meshNames
- #split it into individual mesh names
- names = _obj.mesh_sequence_settings.meshNames.split('/')
- #return the one at _idx
- name = names[_idx]
- return bpy.data.meshes[name]
-
- def setFrame(self, _frame):
- #for each object in the scene
- for obj in bpy.data.objects:
- #if it's been loaded and initialized
- if obj.mesh_sequence_settings.initialized == True and obj.mesh_sequence_settings.loaded == True:
- #set the frame on the sequence object
- self.setFrameObj(obj, _frame)
-
- def getMeshIdxFromFrame(self, _obj, _frameNum):
- numFrames = _obj.mesh_sequence_settings.numMeshes - 1
- #convert the frame number into an array index
- idx = _frameNum - (_obj.mesh_sequence_settings.startFrame - 1)
- #adjust for playback speed
- idx = int(idx * _obj.mesh_sequence_settings.speed)
- #get the playback mode
- frameMode = int(_obj.mesh_sequence_settings.frameMode)
- #0: Blank
- if(frameMode == 0):
- if(idx < 1 or idx >= numFrames + 1):
- idx = 0
- #1: Extend (default)
- elif(frameMode == 1):
- if(idx < 1):
- idx = 1
- elif(idx >= numFrames + 1):
- idx = numFrames
- #2: Repeat
- elif(frameMode == 2):
- idx -= 1
- idx = idx % (numFrames)
- idx += 1
- #3: Bounce
- elif(frameMode == 3):
- idx -= 1
- tmp = int(idx / numFrames)
- if(tmp % 2 == 0):
- idx = idx % numFrames
- else:
- idx = (numFrames - 1) - (idx % numFrames)
- idx += 1
- return idx
-
- def setFrameObj(self, _obj, _frameNum):
- idx = self.getMeshIdxFromFrame(_obj, _frameNum)
- #store the current mesh for grabbing the material later
- prev_mesh = _obj.data
- #swap the meshes
- _obj.data = self.getMesh(_obj, idx)
- # If this object doesn't have materials for each frame
- if(_obj.mesh_sequence_settings.perFrameMaterial == False):
- #if the previous mesh had a material, copy it to the new one
- if(len(prev_mesh.materials) > 0):
- prev_material = prev_mesh.materials[0]
- _obj.data.materials.clear()
- _obj.data.materials.append(prev_material)
-
- #iterate over the meshes in the sequence and set their shading to smooth or flat
- def shadeSequence(self, _obj, _smooth):
- scn = bpy.context.scene
- #deselect everything in the scene
- deselectAll()
- #select the sequence object
- scn.objects.active = _obj
- _obj.select = True
- #grab the current mesh so we can put it back later
- origMesh = _obj.data
- #for each mesh in the sequence
- for idx in range(1, _obj.mesh_sequence_settings.numMeshes):
- #set the object's mesh to this mesh
- _obj.data = self.getMesh(_obj, idx)
- if(_smooth):
- #call Blender's shade_smooth operator
- bpy.ops.object.shade_smooth()
- else:
- #call Blender's shade_flat operator
- bpy.ops.object.shade_flat()
- #reset the sequence's mesh to the right one based on the current frame
- _obj.data = origMesh
-
-
- #create a separate object for each mesh in the array, each visible for only one frame
- def bakeSequence(self, _obj):
- scn = bpy.context.scene
- #create an empty object
- bpy.ops.object.empty_add(type='PLAIN_AXES')
- containerObj = bpy.context.active_object
- #rename the container object to "C_{object's current name}" ('C' stands for 'Container')
- newName = "C_" + _obj.name
- containerObj.name = newName
-
- #copy the object's transformation data into the container
- containerObj.location = _obj.location
- containerObj.scale = _obj.scale
- containerObj.rotation_euler = _obj.rotation_euler
- containerObj.rotation_quaternion = _obj.rotation_quaternion #just in case
-
- #copy the object's animation data into the container
- #http://blender.stackexchange.com/questions/27136/how-to-copy-keyframes-from-one-action-to-other
- if(_obj.animation_data != None):
- seq_anim = _obj.animation_data
- properties = [p.identifier for p in seq_anim.bl_rna.properties if not p.is_readonly]
- if(containerObj.animation_data == None):
- containerObj.animation_data_create()
- cont_anim = containerObj.animation_data
- for prop in properties:
- setattr(cont_anim, prop, getattr(seq_anim, prop))
-
- #create a dictionary mapping meshes to new objects, meshToObject
- meshToObject = {}
-
- meshNames = _obj.mesh_sequence_settings.meshNames.split('/')
- #for each mesh (including the empty mesh):
- for meshName in meshNames:
- mesh = bpy.data.meshes[meshName]
- #even though it's kinda still part of a mesh sequence, it's not really anymore
- mesh.inMeshSequence = False
- #create an object for the mesh and add it to the scene
- tmpObj = bpy.data.objects.new('o_' + mesh.name, mesh)
- scn.objects.link(tmpObj)
- #remove the fake user from the mesh
- mesh.use_fake_user = False
- #add a dictionary entry to meshToObject, the mesh => the object
- meshToObject[mesh] = tmpObj
- #in the object, add keyframes at frames 0 and the last frame of the animation:
- #set object.hide to True
- tmpObj.hide = True
- tmpObj.keyframe_insert(data_path='hide', frame=scn.frame_start)
- tmpObj.keyframe_insert(data_path='hide', frame=scn.frame_end)
- #set object.hide_render to True
- tmpObj.hide_render = True
- tmpObj.keyframe_insert(data_path='hide_render', frame=scn.frame_start)
- tmpObj.keyframe_insert(data_path='hide_render', frame=scn.frame_end)
- #set the empty object as this object's parent
- tmpObj.parent = containerObj
-
- #If this is a single-material sequence, make sure the material is copied to the whole sequence
- #This assumes that the first mesh in the sequence has a material
- if(_obj.mesh_sequence_settings.perFrameMaterial == False):
- #grab the materials from the first frame
- objMaterials = bpy.data.meshes[meshNames[1]].materials
- #for each mesh:
- iterMeshes = iter(meshNames)
- next(iterMeshes) #skip the emptyMesh
- next(iterMeshes) #skip the first mesh (we'll copy the material from this one into the rest of them)
- for meshName in iterMeshes:
- mesh = bpy.data.meshes[meshName]
- #set the material to objMaterial
- mesh.materials.clear()
- for material in objMaterials:
- mesh.materials.append(material)
-
- #for each frame of the animation:
- for frameNum in range(scn.frame_start, scn.frame_end + 1):
- #figure out which mesh is visible
- idx = self.getMeshIdxFromFrame(_obj, frameNum)
- frameMesh = self.getMesh(_obj, idx)
- #use the dictionary to find which object the mesh belongs to
- frameObj = meshToObject[frameMesh]
- #add two keyframes to the object at the current frame:
- #set object.hide to False
- frameObj.hide = False
- frameObj.keyframe_insert(data_path='hide', frame=frameNum)
- #set object.hide_render to False
- frameObj.hide_render = False
- frameObj.keyframe_insert(data_path='hide_render', frame=frameNum)
- #add two keyframes to the object at the next frame:
- #set object.hide to True
- frameObj.hide = True
- frameObj.keyframe_insert(data_path='hide', frame=frameNum+1)
- #set object.hide_render to True
- frameObj.hide_render = True
- frameObj.keyframe_insert(data_path='hide_render', frame=frameNum+1)
-
- #delete the sequence object
- deselectAll()
- scn.objects.active = _obj
- _obj.select = True
- bpy.ops.object.delete()
-
- def freeUnusedMeshes(self):
- numFreed = 0
- #for every mesh in the scene
- for t_mesh in bpy.data.meshes:
- #if inMeshSequence is set to True
- if t_mesh.inMeshSequence == True:
- #set its use_fake_user to False
- t_mesh.use_fake_user = False
- numFreed += 1
- #for every object in the scene
- for t_obj in bpy.data.objects:
- #if its mesh sequence has been initialized and loaded
- if t_obj.mesh_sequence_settings.initialized == True and t_obj.mesh_sequence_settings.loaded == True:
- #get its meshNames
- t_meshNames = t_obj.mesh_sequence_settings.meshNames
- #for each mesh name
- for t_meshName in t_meshNames.split('/'):
- #set its use_fake_user to True
- bpy.data.meshes[t_meshName].use_fake_user = True
- numFreed -= 1
- #the remaining meshes with no real or fake users will be garbage collected when Blender is closed
- print(numFreed, " meshes freed")
-
-#COMMENT THIS persistent OUT WHEN RUNNING FROM THE TEXT EDITOR
-@persistent
-def initSequenceController(dummy): #apparently we need a dummy variable?
- global MSC
- #create a new MeshSequenceController object
- MSC = MeshSequenceController()
-
-
-#Add mesh sequence operator
-class AddMeshSequence(bpy.types.Operator):
- """Add Mesh Sequence"""
- #what the operator is called
- bl_idname = "ms.add_mesh_sequence"
- #what shows up in the menu
- bl_label = "Mesh Sequence"
- bl_options = {'UNDO'}
-
- def execute(self, context):
- global MSC
- obj = MSC.newMeshSequence()
-
- return {'FINISHED'}
-
-#the function for adding "Mesh Sequence" to the Add > Mesh menu
-def menu_func(self, context):
- self.layout.operator(AddMeshSequence.bl_idname, icon="PLUGIN")
-
-class LoadMeshSequence(bpy.types.Operator):
- """Load Mesh Sequence"""
- bl_idname = "ms.load_mesh_sequence"
- bl_label = "Load Mesh Sequence"
- bl_options = {'UNDO'}
-
- def execute(self, context):
- global MSC
- obj = context.object
- #get the object's file path
- dirPath = obj.mesh_sequence_settings.dirPath
- #get the object's filename
- fileName = obj.mesh_sequence_settings.fileName
-
- num = MSC.loadSequenceFromFile(obj, dirPath, fileName)
- if(num == 0):
- self.report({'ERROR'}, "No matching files found. Make sure the Root Folder, File Name, and File Format are correct.")
- return {'CANCELLED'}
-
- return {'FINISHED'}
-
-class BatchShadeSmooth(bpy.types.Operator):
- """Smooth Shade Sequence"""
- bl_idname = "ms.batch_shade_smooth"
- bl_label = "Smooth"
- bl_options = {'UNDO'}
-
- def execute(self, context):
- global MSC
- obj = context.object
- MSC.shadeSequence(obj, True) #True for smooth
- return {'FINISHED'}
-
-class BatchShadeFlat(bpy.types.Operator):
- """Flat Shade Sequence"""
- bl_idname = "ms.batch_shade_flat"
- bl_label = "Flat"
- bl_options = {'UNDO'}
-
- def execute(self, context):
- global MSC
- obj = context.object
- MSC.shadeSequence(obj, False) #False for flat
- return {'FINISHED'}
-
-class BakeMeshSequence(bpy.types.Operator):
- """Bake Mesh Sequence"""
- bl_idname = "ms.bake_sequence"
- bl_label = "Bake Mesh Sequence"
- bl_options = {'UNDO'}
-
- def execute(self, context):
- global MSC
- obj = context.object
- MSC.bakeSequence(obj)
- #update the frame so the right shape is visible
- bpy.context.scene.frame_current = bpy.context.scene.frame_current
- return {'FINISHED'}
-
-#The properties panel added to the Object Properties Panel list
-class MeshSequencePanel(bpy.types.Panel):
- bl_idname = 'OBJ_SEQUENCE_properties'
- bl_label = 'Mesh Sequence' #The name that will show up in the properties panel
- bl_space_type = 'PROPERTIES'
- bl_region_type = 'WINDOW'
- bl_context = 'object'
-
- def draw(self, context):
- layout = self.layout
- obj = context.object
-
- objSettings = obj.mesh_sequence_settings
- if(objSettings.initialized == True):
- #Only show options for loading a sequence if one hasn't been loaded yet
- if(objSettings.loaded == False):
- layout.label("Load Mesh Sequence:", icon='FILE_FOLDER')
- #path to directory
- row = layout.row()
- row.prop(objSettings, "dirPath")
-
- #filename
- row = layout.row()
- row.prop(objSettings, "fileName")
-
- #file extension
- row = layout.row()
- row.prop(objSettings, "fileFormat")
-
- #material mode (one material total or one material per frame)
- row = layout.row()
- row.prop(objSettings, "perFrameMaterial")
-
- #button for loading
- row = layout.row()
- row.operator("ms.load_mesh_sequence")
-
- if(objSettings.loaded == True):
- #start frame
- row = layout.row()
- row.prop(objSettings, "startFrame")
-
- #frame mode
- row = layout.row()
- row.prop(objSettings, "frameMode")
-
- #playback speed
- row = layout.row()
- row.prop(objSettings, "speed")
-
- #Show the shading buttons only if a sequence has been loaded
- layout.row().separator()
- row = layout.row(align=True)
- row.label("Shading:")
- row.operator("ms.batch_shade_smooth")
- row.operator("ms.batch_shade_flat")
-
- #Bake Sequence button
- layout.row().separator()
- row = layout.row()
- box = row.box()
- box.operator("ms.bake_sequence")
-
-def register():
- #give bpy.types.Mesh a new property that says whether it's part of a mesh sequence
- bpy.types.Mesh.inMeshSequence = bpy.props.BoolProperty() #defaults to False
- #register this settings class
- bpy.utils.register_class(MeshSequenceSettings)
- #add this settings class to bpy.types.Object
- bpy.types.Object.mesh_sequence_settings = bpy.props.PointerProperty(type=MeshSequenceSettings)
- bpy.app.handlers.load_post.append(initSequenceController)
- bpy.app.handlers.frame_change_pre.append(updateFrame)
- bpy.utils.register_class(AddMeshSequence)
- bpy.utils.register_class(LoadMeshSequence)
- bpy.utils.register_class(BatchShadeSmooth)
- bpy.utils.register_class(BatchShadeFlat)
- bpy.utils.register_class(BakeMeshSequence)
- bpy.utils.register_class(MeshSequencePanel)
- bpy.types.INFO_MT_mesh_add.append(menu_func)
- #for running the script, instead of installing the add-on
- #UNCOMMENT THIS FUNCTION CALL WHEN RUNNING FROM THE TEXT EDITOR
- #initSequenceController(0)
-
-def unregister():
- bpy.app.handlers.load_post.remove(initSequenceController)
- bpy.app.handlers.frame_change_pre.remove(updateFrame)
- bpy.utils.unregister_class(AddMeshSequence)
- bpy.utils.unregister_class(LoadMeshSequence)
- bpy.utils.unregister_class(BatchShadeSmooth)
- bpy.utils.unregister_class(BatchShadeFlat)
- bpy.utils.unregister_class(BakeMeshSequence)
- bpy.utils.unregister_class(MeshSequencePanel)
- bpy.types.INFO_MT_mesh_add.remove(menu_func)
-
-if __name__ == "__main__":
- register()
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# Stop motion OBJ: A Mesh sequence importer for Blender
+# Copyright (C) 2016-2019 Justin Jensen
+#
+# 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 3 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, see .
+#
+# ##### END GPL LICENSE BLOCK #####
+
+
+bl_info = {
+ "name" : "Stop motion OBJ",
+ "description": "Import a sequence of OBJ (or STL or PLY) files and display them each as a single frame of animation. This add-on also supports the .STL and .PLY file formats.",
+ "author": "Justin Jensen",
+ "version": (2, 0, 0),
+ "blender": (2, 80, 0),
+ "location": "View 3D > Add > Mesh > Mesh Sequence",
+ "warning": "",
+ "category": "Add Mesh",
+ "wiki_url": "https://github.com/neverhood311/Stop-motion-OBJ",
+ "tracker_url": "https://github.com/neverhood311/Stop-motion-OBJ/issues"
+}
+
+import bpy
+import os
+import re
+import glob
+from bpy.app.handlers import persistent
+
+def alphanumKey(string):
+ """ Turn a string into a list of string and number chunks.
+ "z23a" -> ["z", 23, "a"]
+ """
+ return [int(c) if c.isdigit() else c for c in re.split('([0-9]+)', string)]
+
+#global variable for the MeshSequenceController
+MSC = None
+
+def deselectAll():
+ for ob in bpy.context.scene.objects:
+ ob.select_set(state=False)
+
+#set the frame number for all mesh sequence objects
+#COMMENT THIS persistent OUT WHEN RUNNING FROM THE TEXT EDITOR
+@persistent
+def updateFrame(dummy):
+ scn = bpy.context.scene
+ global MSC
+ MSC.setFrame(scn.frame_current)
+
+#runs every time the start frame of an object is changed
+def updateStartFrame(self, context):
+ updateFrame(0)
+ return None
+
+def countMatchingFiles(_directory, _filePrefix, _fileExtension):
+ full_filepath = os.path.join(_directory, _filePrefix + '*.' + _fileExtension)
+ print(full_filepath)
+ files = glob.glob(full_filepath)
+ print(files)
+ return len(files)
+
+def fileExtensionFromTypeNumber(_typeNumber):
+ #OBJ
+ if(_typeNumber == 0):
+ return 'obj'
+ #STL
+ elif(_typeNumber == 1):
+ return 'stl'
+ #PLY
+ elif(_typeNumber == 2):
+ return 'ply'
+ return ''
+
+def importFuncFromTypeNumber(_typeNumber):
+ # OBJ
+ if (_typeNumber == 0):
+ return bpy.ops.import_scene.obj
+ # STL
+ elif (_typeNumber == 1):
+ return bpy.ops.import_mesh.stl
+ # PLY
+ elif (_typeNumber == 2):
+ return bpy.ops.import_mesh.ply
+ return None
+
+class MeshSequenceSettings(bpy.types.PropertyGroup):
+ dirPath: bpy.props.StringProperty(
+ name="Root Folder",
+ description="Only .OBJ files will be listed",
+ subtype="DIR_PATH")
+ fileName: bpy.props.StringProperty(name='File Name')
+ startFrame: bpy.props.IntProperty(
+ name='Start Frame',
+ update=updateStartFrame,
+ default=1)
+ #A long list of mesh names
+ meshNames: bpy.props.StringProperty()
+ numMeshes: bpy.props.IntProperty()
+ initialized: bpy.props.BoolProperty(default=False)
+ loaded: bpy.props.BoolProperty(default=False)
+
+ #out-of-range frame mode
+ frameMode: bpy.props.EnumProperty(
+ items = [('0', 'Blank', 'Object disappears when frame is out of range'),
+ ('1', 'Extend', 'First and last frames are duplicated'),
+ ('2', 'Repeat', 'Repeat the animation'),
+ ('3', 'Bounce', 'Play in reverse at the end of the frame range')],
+ name='Frame Mode',
+ default='1')
+
+ #material mode (one material total or one material per frame)
+ perFrameMaterial: bpy.props.BoolProperty(
+ name='Material per Frame',
+ default=False
+ )
+
+ #playback speed
+ speed: bpy.props.FloatProperty(
+ name='Playback Speed',
+ min=0.0001,
+ soft_min=0.01,
+ step=25,
+ precision=2,
+ default=1
+ )
+
+ #the file format for files in the sequence (OBJ, STL, or PLY)
+ fileFormat: bpy.props.EnumProperty(
+ items = [('0', 'OBJ', 'Wavefront OBJ'),
+ ('1', 'STL', 'STereoLithography'),
+ ('2', 'PLY', 'Stanford PLY')],
+ name='File Format',
+ default='0')
+
+class MeshSequenceController:
+
+ def __init__(self):
+ #for each object in bpy.data.objects:
+ for obj in bpy.data.objects:
+ #for obj in bpy.context.scene.objects:
+ #if it's a sequence object (we'll have to figure out how to indicate this, probably with a T/F custom property)
+ if(obj.mesh_sequence_settings.initialized == True):
+ #call sequence.loadSequenceFromData() on it
+ self.loadSequenceFromData(obj)
+ self.freeUnusedMeshes()
+
+ def newMeshSequence(self):
+ #create a mesh object
+ bpy.ops.object.add(type='MESH')
+ #get a reference to it
+ theObj = bpy.context.object
+ #change its name to 'sequence'
+ theObj.name = 'sequence'
+ #grab its mesh data and change its name to 'emptyMesh'
+ theMesh = theObj.data
+ theMesh.name = 'emptyMesh'
+ #give the empty mesh a fake user
+ theMesh.use_fake_user = True
+ #make sure it knows it's part of a mesh sequence
+ theMesh.inMeshSequence = True
+ #add the mesh's name to the object's mesh_sequence_settings
+ theObj.mesh_sequence_settings.meshNames = theMesh.name + '/'
+
+ #deselect all other objects
+ deselectAll()
+
+ #select the object
+ scn = bpy.context.scene
+ #scn.objects.active = theObj
+ theObj.select_set(state=True)
+
+ theObj.mesh_sequence_settings.initialized = True
+ return theObj
+
+ def loadSequenceFromFile(self, _obj, _dir, _file):
+ # make sure the empty sequence object is deselected
+ deselectAll()
+
+ # error out early if there are no files that match the file prefix
+ fileExtension = fileExtensionFromTypeNumber(int(_obj.mesh_sequence_settings.fileFormat))
+ if countMatchingFiles(_dir, _file, fileExtension) == 0:
+ return 0
+
+ scn = bpy.context.scene
+ # get the file format
+ formatidx = int(_obj.mesh_sequence_settings.fileFormat)
+ importFunc = importFuncFromTypeNumber(int(_obj.mesh_sequence_settings.fileFormat))
+
+ # combine the file directory with the filename and the .obj extension
+ full_dirpath = bpy.path.abspath(_dir)
+ full_filepath = os.path.join(full_dirpath, _file + '*.' + fileExtension)
+
+ numFrames = 0
+ unsortedFiles = glob.glob(full_filepath)
+ # Sort the given list in the way that humans expect.
+ sortedFiles = sorted(unsortedFiles, key=alphanumKey)
+
+ # for each file that matches the glob query:
+ for file in sortedFiles:
+ # import the mesh file
+ importFunc(filepath = file)
+ # get a reference to it
+ tmpObject = bpy.context.selected_objects[0]
+ tmpMesh = tmpObject.data # don't copy it; just copy the pointer. This cuts memory usage in half.
+ tmpMesh.use_fake_user = True
+ tmpMesh.inMeshSequence = True
+ # deselect all objects
+ deselectAll()
+ # select the object
+ tmpObject.select_set(state=True)
+ # delete it
+ bpy.ops.object.delete()
+ # add the new mesh's name to the sequence object's text property
+ # add the '/' character as a delimiter
+ # http://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
+ _obj.mesh_sequence_settings.meshNames += tmpMesh.name + '/'
+ numFrames+=1
+
+ _obj.mesh_sequence_settings.numMeshes = numFrames+1
+ if(numFrames > 0):
+ # remove the last '/' from the string
+ _obj.mesh_sequence_settings.meshNames = _obj.mesh_sequence_settings.meshNames[:-1]
+ # set the sequence object's mesh to meshes[1]
+ self.setFrameObj(_obj, scn.frame_current)
+
+ # select the sequence object
+ _obj.select_set(state=True)
+
+ _obj.mesh_sequence_settings.loaded = True
+
+ return numFrames
+
+ #this is used when a mesh sequence object has been saved and subsequently found in a .blend file
+ def loadSequenceFromData(self, _obj):
+ scn = bpy.context.scene
+ t_meshNames = _obj.mesh_sequence_settings.meshNames.split('/')
+ #count the number of mesh names
+ #(helps with backwards compatibility)
+ _obj.mesh_sequence_settings.numMeshes = len(t_meshNames)
+
+ #make sure the meshes know they're part of a mesh sequence
+ #(helps with backwards compatibility)
+ for t_meshName in t_meshNames:
+ bpy.data.meshes[t_meshName].inMeshSequence = True
+
+ #deselect all objects (otherwise everything that is selected will get deleted)
+ deselectAll()
+
+ #select the sequence object
+ scn.objects.active = _obj
+ _obj.select = True
+ #set the frame number
+ self.setFrameObj(_obj, scn.frame_current)
+
+ _obj.mesh_sequence_settings.loaded = True
+
+ def reloadSequenceFromFile(self, _object, _directory, _filePrefix):
+ # if there are no files that match the file prefix, error out early before making changes
+ fileExtension = fileExtensionFromTypeNumber(int(_object.mesh_sequence_settings.fileFormat))
+ if countMatchingFiles(_directory, _filePrefix, fileExtension) == 0:
+ return 0
+
+ # mark the existing meshes for cleanup (keep the first 'emptyMesh' one)
+ for meshName in _object.mesh_sequence_settings.meshNames.split('/')[1:]:
+ # get rid of its fake user
+ bpy.data.meshes[meshName].use_fake_user = False
+
+ # set its inMeshSequence to false
+ bpy.data.meshes[meshName].inMeshSequence = False
+
+ # re-initialize _object.meshNames
+ _object.mesh_sequence_settings.meshNames = 'emptyMesh/'
+
+ # temporarily set the speed to 1 while we reload
+ originalSpeed = _object.mesh_sequence_settings.speed
+ _object.mesh_sequence_settings.speed = 1.0
+
+ numMeshes = self.loadSequenceFromFile(_object, _directory, _filePrefix)
+
+ # set the speed back to its previous value
+ _object.mesh_sequence_settings.speed = originalSpeed
+
+ return numMeshes
+
+ def getMesh(self, _obj, _idx):
+ #get the object's meshNames
+ #split it into individual mesh names
+ names = _obj.mesh_sequence_settings.meshNames.split('/')
+ #return the one at _idx
+ name = names[_idx]
+ return bpy.data.meshes[name]
+
+ def setFrame(self, _frame):
+ #for each object in the scene
+ for obj in bpy.data.objects:
+ #if it's been loaded and initialized
+ if obj.mesh_sequence_settings.initialized == True and obj.mesh_sequence_settings.loaded == True:
+ #set the frame on the sequence object
+ self.setFrameObj(obj, _frame)
+
+ def getMeshIdxFromFrame(self, _obj, _frameNum):
+ numFrames = _obj.mesh_sequence_settings.numMeshes - 1
+ #convert the frame number into an array index
+ idx = _frameNum - (_obj.mesh_sequence_settings.startFrame - 1)
+ #adjust for playback speed
+ idx = int(idx * _obj.mesh_sequence_settings.speed)
+ #get the playback mode
+ frameMode = int(_obj.mesh_sequence_settings.frameMode)
+ #0: Blank
+ if(frameMode == 0):
+ if(idx < 1 or idx >= numFrames + 1):
+ idx = 0
+ #1: Extend (default)
+ elif(frameMode == 1):
+ if(idx < 1):
+ idx = 1
+ elif(idx >= numFrames + 1):
+ idx = numFrames
+ #2: Repeat
+ elif(frameMode == 2):
+ idx = ((idx - 1) % (numFrames)) + 1
+ #3: Bounce
+ elif(frameMode == 3):
+ idx -= 1
+ tmp = int(idx / numFrames)
+ if(tmp % 2 == 0):
+ idx = idx % numFrames
+ else:
+ idx = (numFrames - 1) - (idx % numFrames)
+ idx += 1
+ return idx
+
+ def setFrameObj(self, _obj, _frameNum):
+ idx = self.getMeshIdxFromFrame(_obj, _frameNum)
+ #store the current mesh for grabbing the material later
+ prev_mesh = _obj.data
+ #swap the meshes
+ _obj.data = self.getMesh(_obj, idx)
+ # If this object doesn't have materials for each frame
+ if(_obj.mesh_sequence_settings.perFrameMaterial == False):
+ #if the previous mesh had a material, copy it to the new one
+ if(len(prev_mesh.materials) > 0):
+ prev_material = prev_mesh.materials[0]
+ _obj.data.materials.clear()
+ _obj.data.materials.append(prev_material)
+
+ #iterate over the meshes in the sequence and set their shading to smooth or flat
+ def shadeSequence(self, _obj, _smooth):
+ scn = bpy.context.scene
+ #deselect everything in the scene
+ deselectAll()
+ #select the sequence object
+ _obj.select_set(state=True)
+ #grab the current mesh so we can put it back later
+ origMesh = _obj.data
+ #for each mesh in the sequence
+ for idx in range(1, _obj.mesh_sequence_settings.numMeshes):
+ #set the object's mesh to this mesh
+ _obj.data = self.getMesh(_obj, idx)
+ if(_smooth):
+ #call Blender's shade_smooth operator
+ bpy.ops.object.shade_smooth()
+ else:
+ #call Blender's shade_flat operator
+ bpy.ops.object.shade_flat()
+ #reset the sequence's mesh to the right one based on the current frame
+ _obj.data = origMesh
+
+
+ #create a separate object for each mesh in the array, each visible for only one frame
+ def bakeSequence(self, _obj):
+ scn = bpy.context.scene
+ activeCollection = bpy.context.collection
+ #create an empty object
+ bpy.ops.object.empty_add(type='PLAIN_AXES')
+ containerObj = bpy.context.active_object
+ #rename the container object to "C_{object's current name}" ('C' stands for 'Container')
+ newName = "C_" + _obj.name
+ containerObj.name = newName
+
+ #copy the object's transformation data into the container
+ containerObj.location = _obj.location
+ containerObj.scale = _obj.scale
+ containerObj.rotation_euler = _obj.rotation_euler
+ containerObj.rotation_quaternion = _obj.rotation_quaternion #just in case
+
+ #copy the object's animation data into the container
+ #http://blender.stackexchange.com/questions/27136/how-to-copy-keyframes-from-one-action-to-other
+ if(_obj.animation_data != None):
+ seq_anim = _obj.animation_data
+ properties = [p.identifier for p in seq_anim.bl_rna.properties if not p.is_readonly]
+ if(containerObj.animation_data == None):
+ containerObj.animation_data_create()
+ cont_anim = containerObj.animation_data
+ for prop in properties:
+ setattr(cont_anim, prop, getattr(seq_anim, prop))
+
+ #create a dictionary mapping meshes to new objects, meshToObject
+ meshToObject = {}
+
+ meshNames = _obj.mesh_sequence_settings.meshNames.split('/')
+ #for each mesh (including the empty mesh):
+ for meshName in meshNames:
+ mesh = bpy.data.meshes[meshName]
+ #even though it's kinda still part of a mesh sequence, it's not really anymore
+ mesh.inMeshSequence = False
+ #create an object for the mesh and add it to the scene
+ tmpObj = bpy.data.objects.new('o_' + mesh.name, mesh)
+ activeCollection.objects.link(tmpObj)
+ #remove the fake user from the mesh
+ mesh.use_fake_user = False
+ #add a dictionary entry to meshToObject, the mesh => the object
+ meshToObject[mesh] = tmpObj
+ #in the object, add keyframes at frames 0 and the last frame of the animation:
+ #set object.hide to True
+ tmpObj.hide_viewport = True
+ tmpObj.keyframe_insert(data_path='hide_viewport', frame=scn.frame_start)
+ tmpObj.keyframe_insert(data_path='hide_viewport', frame=scn.frame_end)
+ #set object.hide_render to True
+ tmpObj.hide_render = True
+ tmpObj.keyframe_insert(data_path='hide_render', frame=scn.frame_start)
+ tmpObj.keyframe_insert(data_path='hide_render', frame=scn.frame_end)
+ #set the empty object as this object's parent
+ tmpObj.parent = containerObj
+
+ #If this is a single-material sequence, make sure the material is copied to the whole sequence
+ #This assumes that the first mesh in the sequence has a material
+ if(_obj.mesh_sequence_settings.perFrameMaterial == False):
+ #grab the materials from the first frame
+ objMaterials = bpy.data.meshes[meshNames[1]].materials
+ #for each mesh:
+ iterMeshes = iter(meshNames)
+ next(iterMeshes) #skip the emptyMesh
+ next(iterMeshes) #skip the first mesh (we'll copy the material from this one into the rest of them)
+ for meshName in iterMeshes:
+ mesh = bpy.data.meshes[meshName]
+ #set the material to objMaterial
+ mesh.materials.clear()
+ for material in objMaterials:
+ mesh.materials.append(material)
+
+ #for each frame of the animation:
+ for frameNum in range(scn.frame_start, scn.frame_end + 1):
+ #figure out which mesh is visible
+ idx = self.getMeshIdxFromFrame(_obj, frameNum)
+ frameMesh = self.getMesh(_obj, idx)
+ #use the dictionary to find which object the mesh belongs to
+ frameObj = meshToObject[frameMesh]
+ #add two keyframes to the object at the current frame:
+ #set object.hide to False
+ frameObj.hide_viewport = False
+ frameObj.keyframe_insert(data_path='hide_viewport', frame=frameNum)
+ #set object.hide_render to False
+ frameObj.hide_render = False
+ frameObj.keyframe_insert(data_path='hide_render', frame=frameNum)
+ #add two keyframes to the object at the next frame:
+ #set object.hide to True
+ frameObj.hide_viewport = True
+ frameObj.keyframe_insert(data_path='hide_viewport', frame=frameNum+1)
+ #set object.hide_render to True
+ frameObj.hide_render = True
+ frameObj.keyframe_insert(data_path='hide_render', frame=frameNum+1)
+
+ #delete the sequence object
+ deselectAll()
+ _obj.select_set(state=True)
+ bpy.ops.object.delete()
+
+ def freeUnusedMeshes(self):
+ numFreed = 0
+ #for every mesh in the scene
+ for t_mesh in bpy.data.meshes:
+ #if inMeshSequence is set to True
+ if t_mesh.inMeshSequence == True:
+ #set its use_fake_user to False
+ t_mesh.use_fake_user = False
+ numFreed += 1
+ #for every object in the scene
+ for t_obj in bpy.data.objects:
+ #if its mesh sequence has been initialized and loaded
+ if t_obj.mesh_sequence_settings.initialized == True and t_obj.mesh_sequence_settings.loaded == True:
+ #get its meshNames
+ t_meshNames = t_obj.mesh_sequence_settings.meshNames
+ #for each mesh name
+ for t_meshName in t_meshNames.split('/'):
+ #set its use_fake_user to True
+ bpy.data.meshes[t_meshName].use_fake_user = True
+ numFreed -= 1
+ #the remaining meshes with no real or fake users will be garbage collected when Blender is closed
+ print(numFreed, " meshes freed")
+
+#COMMENT THIS persistent OUT WHEN RUNNING FROM THE TEXT EDITOR
+@persistent
+def initSequenceController(dummy): #apparently we need a dummy variable?
+ global MSC
+ #create a new MeshSequenceController object
+ MSC = MeshSequenceController()
+
+
+#Add mesh sequence operator
+class AddMeshSequence(bpy.types.Operator):
+ """Add Mesh Sequence"""
+ #what the operator is called
+ bl_idname = "ms.add_mesh_sequence"
+ #what shows up in the menu
+ bl_label = "Mesh Sequence"
+ bl_options = {'UNDO'}
+
+ def execute(self, context):
+ global MSC
+ obj = MSC.newMeshSequence()
+
+ return {'FINISHED'}
+
+#the function for adding "Mesh Sequence" to the Add > Mesh menu
+def menu_func(self, context):
+ self.layout.operator(AddMeshSequence.bl_idname, icon="PLUGIN")
+
+class LoadMeshSequence(bpy.types.Operator):
+ """Load Mesh Sequence"""
+ bl_idname = "ms.load_mesh_sequence"
+ bl_label = "Load Mesh Sequence"
+ bl_options = {'UNDO'}
+
+ def execute(self, context):
+ global MSC
+ obj = context.object
+ #get the object's file path
+ dirPath = obj.mesh_sequence_settings.dirPath
+ #get the object's filename
+ fileName = obj.mesh_sequence_settings.fileName
+
+ num = MSC.loadSequenceFromFile(obj, dirPath, fileName)
+ if(num == 0):
+ self.report({'ERROR'}, "No matching files found. Make sure the Root Folder, File Name, and File Format are correct.")
+ return {'CANCELLED'}
+
+ return {'FINISHED'}
+
+class ReloadMeshSequence(bpy.types.Operator):
+ """Reload From Disk"""
+ bl_idname = "ms.reload_mesh_sequence"
+ bl_label = "Reload From Disk"
+ bl_options = {'UNDO'}
+
+ def execute(self, context):
+ global MSC
+ obj = context.object
+
+ # get the object's file path
+ dirPath = obj.mesh_sequence_settings.dirPath
+
+ # get the object's filename
+ fileName = obj.mesh_sequence_settings.fileName
+
+ num = MSC.reloadSequenceFromFile(obj, dirPath, fileName)
+ if (num == 0):
+ self.report({'ERROR'}, "Invalid file path. Make sure the Root Folder, File Name, and File Format are correct.")
+ return {'CANCELLED'}
+
+ return {'FINISHED'}
+
+class BatchShadeSmooth(bpy.types.Operator):
+ """Smooth Shade Sequence"""
+ bl_idname = "ms.batch_shade_smooth"
+ bl_label = "Smooth"
+ bl_options = {'UNDO'}
+
+ def execute(self, context):
+ global MSC
+ obj = context.object
+ MSC.shadeSequence(obj, True) #True for smooth
+ return {'FINISHED'}
+
+class BatchShadeFlat(bpy.types.Operator):
+ """Flat Shade Sequence"""
+ bl_idname = "ms.batch_shade_flat"
+ bl_label = "Flat"
+ bl_options = {'UNDO'}
+
+ def execute(self, context):
+ global MSC
+ obj = context.object
+ MSC.shadeSequence(obj, False) #False for flat
+ return {'FINISHED'}
+
+class BakeMeshSequence(bpy.types.Operator):
+ """Bake Mesh Sequence"""
+ bl_idname = "ms.bake_sequence"
+ bl_label = "Bake Mesh Sequence"
+ bl_options = {'UNDO'}
+
+ def execute(self, context):
+ global MSC
+ obj = context.object
+ MSC.bakeSequence(obj)
+ #update the frame so the right shape is visible
+ bpy.context.scene.frame_current = bpy.context.scene.frame_current
+ return {'FINISHED'}
+
+#The properties panel added to the Object Properties Panel list
+class MeshSequencePanel(bpy.types.Panel):
+ bl_idname = 'OBJ_SEQUENCE_properties'
+ bl_label = 'Mesh Sequence' #The name that will show up in the properties panel
+ bl_space_type = 'PROPERTIES'
+ bl_region_type = 'WINDOW'
+ bl_context = 'object'
+
+ def draw(self, context):
+ layout = self.layout
+ obj = context.object
+
+ objSettings = obj.mesh_sequence_settings
+ if(objSettings.initialized == True):
+ #Only show options for loading a sequence if one hasn't been loaded yet
+ if(objSettings.loaded == False):
+ layout.label(text= "Load Mesh Sequence:", icon='FILE_FOLDER')
+ #path to directory
+ row = layout.row()
+ row.prop(objSettings, "dirPath")
+
+ #filename
+ row = layout.row()
+ row.prop(objSettings, "fileName")
+
+ #file extension
+ row = layout.row()
+ row.prop(objSettings, "fileFormat")
+
+ #material mode (one material total or one material per frame)
+ row = layout.row()
+ row.prop(objSettings, "perFrameMaterial")
+
+ #button for loading
+ row = layout.row()
+ row.operator("ms.load_mesh_sequence")
+
+ if(objSettings.loaded == True):
+ #start frame
+ row = layout.row()
+ row.prop(objSettings, "startFrame")
+
+ #frame mode
+ row = layout.row()
+ row.prop(objSettings, "frameMode")
+
+ #playback speed
+ row = layout.row()
+ row.prop(objSettings, "speed")
+
+ # Reload From Disk button
+ row = layout.row()
+ row.operator("ms.reload_mesh_sequence")
+
+ #Show the shading buttons only if a sequence has been loaded
+ layout.row().separator()
+ row = layout.row(align=True)
+ row.label(text="Shading:")
+ row.operator("ms.batch_shade_smooth")
+ row.operator("ms.batch_shade_flat")
+
+ #Bake Sequence button
+ layout.row().separator()
+ row = layout.row()
+ box = row.box()
+ box.operator("ms.bake_sequence")
+
+def register():
+ #give bpy.types.Mesh a new property that says whether it's part of a mesh sequence
+ bpy.types.Mesh.inMeshSequence = bpy.props.BoolProperty() #defaults to False
+ #register this settings class
+ bpy.utils.register_class(MeshSequenceSettings)
+ #add this settings class to bpy.types.Object
+ bpy.types.Object.mesh_sequence_settings = bpy.props.PointerProperty(type=MeshSequenceSettings)
+ bpy.app.handlers.load_post.append(initSequenceController)
+ bpy.app.handlers.frame_change_pre.append(updateFrame)
+ bpy.utils.register_class(AddMeshSequence)
+ bpy.utils.register_class(LoadMeshSequence)
+ bpy.utils.register_class(ReloadMeshSequence)
+ bpy.utils.register_class(BatchShadeSmooth)
+ bpy.utils.register_class(BatchShadeFlat)
+ bpy.utils.register_class(BakeMeshSequence)
+ bpy.utils.register_class(MeshSequencePanel)
+ bpy.types.VIEW3D_MT_mesh_add.append(menu_func)
+ #for running the script, instead of installing the add-on
+ #UNCOMMENT THIS FUNCTION CALL WHEN RUNNING FROM THE TEXT EDITOR
+ #initSequenceController(0)
+
+def unregister():
+ bpy.app.handlers.load_post.remove(initSequenceController)
+ bpy.app.handlers.frame_change_pre.remove(updateFrame)
+ bpy.utils.unregister_class(AddMeshSequence)
+ bpy.utils.unregister_class(LoadMeshSequence)
+ bpy.utils.unregister_class(ReloadMeshSequence)
+ bpy.utils.unregister_class(BatchShadeSmooth)
+ bpy.utils.unregister_class(BatchShadeFlat)
+ bpy.utils.unregister_class(BakeMeshSequence)
+ bpy.utils.unregister_class(MeshSequencePanel)
+ bpy.types.VIEW3D_MT_mesh_add.remove(menu_func)
+
+if __name__ == "__main__":
+ register()