added ability to write .avi file using blender and automatically open it with 'open' command.
modify and use at own risk.
Happy Sketching!
Nate
#naPlayblastAddOn.py # #This allows playblast of certain frames in blender while filling in the gaps automatically #it allows to work in spline yet make a playblast on two's # #use and modify at your own risk #example usage it assumes first frame in list is starting frame: #enter output image directory and number of frames to skip. click playblast button. # #TODO: allow more than just tiff output format #TODO: allow custom output image prefix # # # #date last modified: 03-16-2019 added .avi writing and auto play of movie after playblast #date last modified: 03-02-2019 initial addon release # # # #inspired by: #Joan Marc Fuentes https://vimeo.com/88955189 #https://stackoverflow.com/questions/14982836/rendering-and-saving-images-through-blender-python #https://blender.stackexchange.com/questions/1101/blender-rendering-automation-build-script #https://blender.stackexchange.com/questions/27579/render-specific-frames-with-opengl-via-python/27640 #https://stackoverflow.com/questions/27515913/create-a-copy-of-an-image-python #https://stackoverflow.com/questions/27678156/how-to-count-by-twos-with-pythons-range #https://stackoverflow.com/questions/3590165/join-a-list-of-items-with-different-types-as-string-in-python #https://blenderartists.org/t/image-sequence-to-movie-using-python-script/587834/2 #https://stackoverflow.com/questions/12368568/play-quicktime-movie-from-terminal bl_info = { "name":"playblast tool", "category": "Object" } import bpy from bpy.props import( StringProperty, IntProperty, PointerProperty ) from bpy.types import( Operator, Panel, PropertyGroup ) class PlayblastOperator(Operator): bl_idname = "obj.do_playblast" bl_label = "Playblast" bl_options = {"REGISTER"} def execute(self, context): #get directory from text field imgDirShort = context.scene.playblast_prop.outImgDir imgDir = bpy.path.abspath(imgDirShort) #because we need long directory path #get skip frame from text field skipFrameArg = context.scene.playblast_prop.numSkipFrame skipFrame = 1 if skipFrameArg == 1 or skipFrameArg == 0: skipFrame = 1 elif skipFrameArg > 1 and skipFrameArg <= (context.scene.frame_end-context.scene.frame_start): skipFrame = skipFrameArg else: self.report({'ERROR'}, "cannot find valid skip frame. check its less than total number frames") return {'FINISHED'} frames = range( context.scene.frame_start, context.scene.frame_end+1, skipFrame) self.report({'INFO'}, "Starting playblast ...") self.report({'INFO'}, "output dir: %s" %imgDir ) self.report({'INFO'}, "playblasting frames: %s" %(' '.join( str(f) for f in frames )) ) #extra bit converting integer list to string naPlayblast( frames = frames, imageDir = imgDir, fill = True) self.report({'INFO'}, "Completed playblast") return {'FINISHED'} class PlayblastPanel(Panel): bl_label = "Playblast Panel" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" def draw(self, context): #here we add textfields and button to ui # layout = self.layout layout.label(text = "Playblast tool") #add image directory text field layout.prop( context.scene.playblast_prop, "outImgDir", text = "out image directory" ) #add frames skip text field layout.prop( context.scene.playblast_prop, "numSkipFrame", text = "number of frames to skip. ex: 2 says playblast every other frame") layout.operator( "obj.do_playblast") class PlayblastProperties(PropertyGroup): #here we make each textfield outImgDir = StringProperty( name = "imageDir", description = "output image directory", subtype = "FILE_PATH" ) numSkipFrame = IntProperty( name = "numSkipFrame", description = "number of frames to skip. ex: 2 means playblast every other frame" ) def register(): bpy.utils.register_class(PlayblastOperator) bpy.utils.register_class(PlayblastPanel) bpy.utils.register_class(PlayblastProperties) #here we name the property that holds all our textfields bpy.types.Scene.playblast_prop = PointerProperty( type = PlayblastProperties ) def unregister(): bpy.utils.unregister_class(PlayblastOperator) bpy.utils.unregister_class(PlayblastPanel) bpy.utils.unregister_class(PlayblastProperties) del bpy.types.Scene.playblast_prop if __name__ == "__main__": register() ##############actual playblast code import shutil import os import bpy def naPlayblast( frames = [1,3,5], imageDir = "/Users/Nathaniel/Desktop/SINGLE_BLENDER_FACE_RIG/intro_blender_pipe/output/tmpPlayblast", fill = True): """ list of frames output image directory fill False means only playblast skip frames and dont fill in missing frames """ if not os.path.exists( imageDir ): print('cannot find output directory') return if frames[0] != bpy.context.scene.frame_start: print('only works with starting frame given matches with timeline') return setImageExt() #playblast chosen frames for frame in frames: bpy.context.scene.frame_set(frame) imageName = getImageNameFromNumber(frame) imagePath = os.path.join( imageDir, imageName ) bpy.data.scenes["Scene"].render.filepath = imagePath bpy.ops.render.opengl(write_still=True) #fill in missing frames if fill: missingInfoDict = getFillMissingFramesDictionary( frames ) fillMissingFrames( missingInfoDict, imageDir ) #done wrting images #write movie file movieName = 'anim.avi' convertImgToVideo( inputDir = imageDir, \ outputDir = imageDir, \ outputFileName = movieName,\ outputFormat = 'AVI_JPEG',\ resolutionX = 720,\ resolutionY = 480 ) #play movie playMovie( movie = os.path.join(imageDir,movieName) ) def setImageExt(): """can change this for different image extension """ bpy.context.scene.render.image_settings.file_format = 'TIFF' def getImageExt(): return 'tif' def getImageNameFromNumber( frame = 1 ): """can change this to any image name formatting """ return 'img_%d.%s' %(frame, getImageExt() ) def getFillMissingFramesDictionary( sourceFrames = [1,3,5] ): """keys are source image name values list of destination image names needed to fill #this gives info for filling in images #ex result given [1,3,5] it assumes always beginning with start frame: #{ 'img_1':['img_2'], 'img_3':[img_4] } """ startFrame = bpy.context.scene.frame_start endFrame = bpy.context.scene.frame_end if sourceFrames[0] != startFrame: print( 'cannot handle source frames not starting at %d.' %startFrame) return result = {} for i in range(0,len(sourceFrames)-1): #print sourceFrames[i], range( sourceFrames[i]+1, sourceFrames[i+1] ) sImage = getImageNameFromNumber( sourceFrames[i] ) dImages = [ getImageNameFromNumber(x) for x in range( sourceFrames[i]+1, sourceFrames[i+1] ) ] result[ sImage ] = dImages #fill in up to total frames if needed if sourceFrames[ len(sourceFrames) - 1 ] != endFrame: sImage = getImageNameFromNumber( sourceFrames[len(sourceFrames)-1] ) dImages = [ getImageNameFromNumber(x) for x in range( sourceFrames[ len(sourceFrames)-1 ]+1, endFrame+1 ) ] result[ sImage ] = dImages return result def fillMissingFrames( imgDict = { 'img_1':['img_2'], 'img_3':['img_4'] }, imgDir = '/Users/Nathaniel/Desktop/SINGLE_BLENDER_FACE_RIG/intro_blender_pipe/output/tmpPlayblast' ): """ this does actual copy of images #given ex: { 'img_1':['img_2'], 'img_3':[img_4] }, imgDir = '/Users/Nathaniel/Desktop/SINGLE_BLENDER_FACE_RIG/intro_blender_pipe/output/tmpPlayblast' """ for sourceImage in imgDict.keys(): destinationImages = imgDict[sourceImage] for dImage in destinationImages: dImageFull = os.path.join( imgDir, dImage ) sImageFull = os.path.join( imgDir, sourceImage ) if not os.path.exists( sImageFull ): print('could not find source image %s, skipping' %sImageFull) continue shutil.copy( sImageFull, dImageFull ) #work on movie writing def convertImgToVideo( inputDir = '/Users/Nathaniel/Desktop/SINGLE_BLENDER_FACE_RIG/intro_blender_pipe/output/tmpPlayblast', \ inputImageExt = 'tif',\ outputDir = '/Users/Nathaniel/Desktop/SINGLE_BLENDER_FACE_RIG/intro_blender_pipe/output/tmpPlayblast', \ outputFileName = 'test.avi', \ outputFormat = 'AVI_JPEG', \ resolutionX = 720 ,\ resolutionY = 480 ): """ convert images in folder to video using blender #other res example 1920x1080 """ #set scene settings bpy.data.scenes["Scene"].render.resolution_x = resolutionX bpy.data.scenes["Scene"].render.resolution_y = resolutionY bpy.data.scenes["Scene"].render.resolution_percentage = 100 #get input images from directory unsortedInputImages = [ path for path in os.listdir(inputDir) if os.path.splitext(path)[1].endswith(inputImageExt) ] #sort input images inputImages = [] tempDict = {} for img in unsortedInputImages: tempDict[ int( (img.split('.')[0]).split('_')[1] ) ] = img for k,v in sorted( tempDict.items() ): inputImages.append( v ) #use blender sequencer inputFiles = [ {"name":i} for i in inputImages ] print(inputFiles) numFrames = len(inputFiles) area = bpy.context.area old_type = area.type area.type = 'SEQUENCE_EDITOR' strip_obj = bpy.ops.sequencer.image_strip_add(directory = inputDir, files = inputFiles, channel = 1, frame_start=0, frame_end = numFrames-1 ) area.type = old_type bpy.data.scenes["Scene"].frame_end = numFrames bpy.data.scenes["Scene"].render.image_settings.file_format = outputFormat bpy.data.scenes["Scene"].render.filepath = os.path.join(outputDir,outputFileName) bpy.ops.render.render(animation = True) #clean up bpy.ops.sequencer.delete() def playMovie(movie = '/Users/Nathaniel/Desktop/SINGLE_BLENDER_FACE_RIG/intro_blender_pipe/output/tmpPlayblast/test.avi'): if not os.path.exists(movie): print('no movie found to play') return os.system( 'open ' + movie )