Saturday, March 16, 2019

Blender playblast tool addon

Hi,

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 )