Saturday, March 2, 2019

Blender Playblast Tool Addon


#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:
#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-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

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 )

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 )
            




Happy Sketching!
Nate