Dev:Py/Scripts/Cookbook/Code snippets/Blender add-ons

提供: wiki
移動先: 案内検索

Blender add-ons

Until now we have only considered stand-alone scripts, which are executed from the Text Editor window. For an end user it is more convenient if the script is a Blender add-on, which can be enabled from the User Preferences window. It is also possible to auto-load a script every time Blender starts.

In order for a script to be an add-on, it must be written in a special way. There must be a bl_info structure in the beginning of the file, and register and unregister functions must be defined at the end. Moreover, the script must be placed at a location where Blender looks for add-ons on upstart. This includes the addons and addons-contrib folders, which are located under the 2.57/scripts subdirectory of the directory where Blender is located.

Shapekey pinning

This script can be executed from the Text Editor window as usual. However, it can also be accessed as a Blender add-on. The add-on info is specified in the bl_info dictonary in the beginning of the file.

bl_info = {
    'name': 'Shapekey pinning',
    'author': 'Thomas Larsson',
    'version': (0, 1, 2),
    'blender': (2, 57, 0),
    'location': 'View3D > UI panel > Shapekey pinning',
    'description': 'Pin and key the shapekeys of a mesh',
    'warning': '',
    'wiki_url': 'http://blenderartists.org/forum/showthread.php?193908',
    'tracker_url': '',
    'support': 'COMMUNITY',
    'category': '3D View'}

The meaning of most slots in this dictionary is self-evident.

  • name: The name of the add-on.
  • author: The author's name.
  • version: The script version.
  • blender: Blender version.
  • location: Where to find the buttons.
  • description: A description displayed as a tooltip and in documentation.
  • warning: A warning message. If not empty, a little warning sign will show up in the user preference window.
  • wiki_url: Link to the script's wiki page. Should really be on the Blender site, but here we are linking to a thread at blenderartists.org.
  • tracker_url: Link to the scripts bug tracker.
  • support: Official or community
  • category: Script category, e.g. 3D View, Import-Export, Add Mesh, orRigging. Correspond to categories in the User Preferences window.

Notes:

  • blender: The version number tuple is comprised of 3 integers, (major, minor, sub), for instance (2, 70, 0) for v2.70 (sub 0), not (2, 7, 0).
  • api: Deprecated attribute, do not use (Blender never tested it against revision number).

Many of the items can simply be omitted, as we will see in other examples below.

The second requirement on an add-on is that it defines register() and unregister() functions, which usually are placed at the end of the file. register() normally makes a call to bpy.utils.register_module(__name__), which registers all classes defined in the file. It can also contain some custom initialization tasks. The present script also declares a custom RNA property. As we saw in subsection RNA properties versus ID properties, declaration is necessary here because the bool property would appear as an integer otherwise.

def register():
    initialize()
    bpy.utils.register_module(__name__)

def unregister():
    bpy.utils.unregister_module(__name__)

if __name__ == "__main__":
    register()

Unregistration is analogous to registration. The last lines makes it possible to run the script in stand-alone mode from the Text Editor window. Even if the end user will never run the script from the editor, it is useful to have this possibility when debugging.

Copy the file to a location where Blender looks for add-ons. Restart Blender. Open the User Preferences window from the File » User Preferences menu, and navigate to the Add-ons tab. Our script can be found at the bottom of the 3D View section.

Code Snippets UserPref.png

We recognize the fields in the bl_info dictionary. Enable the script by pressing the checkbox in the upper-right corner. If you want the add-on to load every time you start Blender, press the Save As Default button at the bottom of the window.

After the add-on has been enabled, it appears in the UI panel.

Code Snippets Addon-enabled.png

The script itself displays the shapekeys of the active object in the UI panel. The default cube does not have any shapekeys. Instead we import a MakeHuman character which has a lot of facial expressions that are implemented as shapekeys. MakeHuman is a application which easily allows you to design a character. A fully rigged and textured character can then be exported to Blender using the MHX (MakeHuman eXchange) format. MHX files can be imported into Blender with the MHX importer, which is an add-on which is distributed with Blender.

What matters for the present example is that the MakeHuman mesh has a lot of shapekeys. If you press the Pin button right to the shapekey value, the shapekey is pinned, i.e. its value is set to one whereas the values of all other shapekeys are zero. If Autokey button in the timeline is set, a key is set for the shapekey value. If furthermore the Key all option in selected, keys are set for every shapekey in the mesh.

Code Snippets Shapepin.png

#----------------------------------------------------------
# File shapekey_pin.py
#----------------------------------------------------------

bl_info = {
    'name': 'Shapekey pinning',
    'author': 'Thomas Larsson',
    'version': (0, 1, 2),
    'blender': (2, 57, 0),
    'location': 'View3D > UI panel > Shapekey pinning',
    'description': 'Pin and key the shapekeys of a mesh',
    'warning': '',
    'wiki_url': 'http://blenderartists.org/forum/showthread.php?193908',
    'tracker_url': '',
    'support': 'COMMUNITY',
    'category': '3D View'}

import bpy
from bpy.props import *

#
#    class VIEW3D_OT_ResetExpressionsButton(bpy.types.Operator):
#
class VIEW3D_OT_ResetExpressionsButton(bpy.types.Operator):
    bl_idname = "shapepin.reset_expressions"
    bl_label = "Reset expressions"

    def execute(self, context):
        keys = context.object.data.shape_keys
        if keys:
            for shape in keys.keys:
                shape.value = 0.0
        return{'FINISHED'}    

#
#    class VIEW3D_OT_PinExpressionButton(bpy.types.Operator):
#

class VIEW3D_OT_PinExpressionButton(bpy.types.Operator):
    bl_idname = "shapepin.pin_expression"
    bl_label = "Pin"
    expression = bpy.props.StringProperty()

    def execute(self, context):
        skeys = context.object.data.shape_keys
        if skeys:
            frame = context.scene.frame_current
            for block in skeys.key_blocks:            
                oldvalue = block.value
                block.value = 1.0 if block.name == self.expression else 0.0
                if (context.tool_settings.use_keyframe_insert_auto and 
                    (context.scene.key_all or 
                    (block.value > 0.01) or 
                    (abs(block.value-oldvalue) > 0.01))):
                    block.keyframe_insert("value", index=-1, frame=frame)
        return{'FINISHED'}    

#
#    class ExpressionsPanel(bpy.types.Panel):
#

class ExpressionsPanel(bpy.types.Panel):
    bl_label = "Pin shapekeys"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    
    @classmethod
    def poll(cls, context):
        return context.object and (context.object.type == 'MESH')

    def draw(self, context):
        layout = self.layout
        layout.operator("shapepin.reset_expressions")
        layout.prop(context.scene, "key_all")
        skeys = context.object.data.shape_keys
        if skeys:
            for block in skeys.key_blocks:            
                row = layout.split(0.75)
                row.prop(block, 'value', text=block.name)
                row.operator("shapepin.pin_expression", 
                    text="Pin").expression = block.name
        return

#
#    initialize and register
#

def initialize():
    bpy.types.Scene.key_all = BoolProperty(
        name="Key all",
        description="Set keys for all shapes",
        default=False)

def register():
    initialize()
    bpy.utils.register_module(__name__)

def unregister():
    bpy.utils.unregister_module(__name__)

if __name__ == "__main__":
    register()

Simple BVH import

The BVH format is commonly used to transfer character animation, e.g. from mocap data. This program is a simple BVH importer, which builds a skeleton with the action described by the BVH file. It is implemented as a Blender add-on, which a bl_info dictionary at the top of the file and registration code at the bottom.

Once the script below has been executed, or enabled as an add-on, the simple BVH importer can be invoked from user interface panel (CtrlN). There are two options: a boolean choice to rotate the mesh 90 degrees (to make Z up), and a scale.

This program also illustrates how to invoke a file selector by pushing a button in a panel. The Load BVH button class inherits both from the bpy.types.Operator and ImportHelper base classes.

class OBJECT_OT_LoadBvhButton(bpy.types.Operator, ImportHelper):

The (possibly undocumented) ImportHelper class defines some attributes which are used to filter the files visible in the file selector.

filename_ext = ".bvh"
filter_glob = bpy.props.StringProperty(default="*.bvh", options={'HIDDEN'})
filepath = bpy.props.StringProperty(name="File Path",
    maxlen=1024, default="")

There is an analogous ExportHelper class which limits the available choices in an export file selector.

Code Snippets Bvh-load.png

#----------------------------------------------------------
# File simple_bvh_import.py
# Simple bvh importer
#----------------------------------------------------------

bl_info = {
    'name': 'Simple BVH importer (.bvh)',
    'author': 'Thomas Larsson',
    'version': (1, 0, 0),
    'blender': (2, 57, 0),
    'location': 'File > Import',
    'description': 'Simple import of Biovision bvh',
    'category': 'Import-Export'}

import bpy, os, math, mathutils, time
from mathutils import Vector, Matrix
from bpy_extras.io_utils import ImportHelper

#
#    class CNode:
#

class CNode:
    def __init__(self, words, parent):
        name = words[1]
        for word in words[2:]:
            name += ' '+word
        
        self.name = name
        self.parent = parent
        self.children = []
        self.head = Vector((0,0,0))
        self.offset = Vector((0,0,0))
        if parent:
            parent.children.append(self)
        self.channels = []
        self.matrix = None
        self.inverse = None
        return

    def __repr__(self):
        return "CNode %s" % (self.name)

    def display(self, pad):
        vec = self.offset
        if vec.length < Epsilon:
            c = '*'
        else:
            c = ' '
        print("%s%s%10s (%8.3f %8.3f %8.3f)" % 
            (c, pad, self.name, vec[0], vec[1], vec[2]))
        for child in self.children:
            child.display(pad+"  ")
        return

    def build(self, amt, orig, parent):
        self.head = orig + self.offset
        if not self.children:
            return self.head
        
        zero = (self.offset.length < Epsilon)
        eb = amt.edit_bones.new(self.name)        
        if parent:
            eb.parent = parent
        eb.head = self.head
        tails = Vector((0,0,0))
        for child in self.children:
            tails += child.build(amt, self.head, eb)
        n = len(self.children)
        eb.tail = tails/n
        (trans,quat,scale) = eb.matrix.decompose()
        self.matrix = quat.to_matrix()
        self.inverse = self.matrix.copy()
        self.inverse.invert()
        if zero:
            return eb.tail
        else:        
            return eb.head

#
#    readBvhFile(context, filepath, rot90, scale):
#

Location = 1
Rotation = 2
Hierarchy = 1
Motion = 2
Frames = 3

Deg2Rad = math.pi/180
Epsilon = 1e-5

def readBvhFile(context, filepath, rot90, scale):
    fileName = os.path.realpath(os.path.expanduser(filepath))
    (shortName, ext) = os.path.splitext(fileName)
    if ext.lower() != ".bvh":
        raise NameError("Not a bvh file: " + fileName)
    print( "Loading BVH file "+ fileName )

    time1 = time.clock()
    level = 0
    nErrors = 0
    scn = context.scene
            
    fp = open(fileName, "rU")
    print( "Reading skeleton" )
    lineNo = 0
    for line in fp: 
        words= line.split()
        lineNo += 1
        if len(words) == 0:
            continue
        key = words[0].upper()
        if key == 'HIERARCHY':
            status = Hierarchy
        elif key == 'MOTION':
            if level != 0:
                raise NameError("Tokenizer out of kilter %d" % level)    
            amt = bpy.data.armatures.new("BvhAmt")
            rig = bpy.data.objects.new("BvhRig", amt)
            scn.objects.link(rig)
            scn.objects.active = rig
            bpy.ops.object.mode_set(mode='EDIT')
            root.build(amt, Vector((0,0,0)), None)
            #root.display('')
            bpy.ops.object.mode_set(mode='OBJECT')
            status = Motion
        elif status == Hierarchy:
            if key == 'ROOT':    
                node = CNode(words, None)
                root = node
                nodes = [root]
            elif key == 'JOINT':
                node = CNode(words, node)
                nodes.append(node)
            elif key == 'OFFSET':
                (x,y,z) = (float(words[1]), float(words[2]), float(words[3]))
                if rot90:                    
                    node.offset = scale*Vector((x,-z,y))
                else:
                    node.offset = scale*Vector((x,y,z))
            elif key == 'END':
                node = CNode(words, node)
            elif key == 'CHANNELS':
                oldmode = None
                for word in words[2:]:
                    if rot90:
                        (index, mode, sign) = channelZup(word)
                    else:
                        (index, mode, sign) = channelYup(word)
                    if mode != oldmode:
                        indices = []
                        node.channels.append((mode, indices))
                        oldmode = mode
                    indices.append((index, sign))
            elif key == '{':
                level += 1
            elif key == '}':
                level -= 1
                node = node.parent
            else:
                raise NameError("Did not expect %s" % words[0])
        elif status == Motion:
            if key == 'FRAMES:':
                nFrames = int(words[1])
            elif key == 'FRAME' and words[1].upper() == 'TIME:':
                frameTime = float(words[2])
                frameTime = 1
                status = Frames
                frame = 0
                t = 0
                bpy.ops.object.mode_set(mode='POSE')
                pbones = rig.pose.bones
                for pb in pbones:
                    pb.rotation_mode = 'QUATERNION'
        elif status == Frames:
            addFrame(words, frame, nodes, pbones, scale)
            t += frameTime
            frame += 1

    fp.close()
    time2 = time.clock()
    print("Bvh file loaded in %.3f s" % (time2-time1))
    return rig

#
#    channelYup(word):
#    channelZup(word):
#

def channelYup(word):
    if word == 'Xrotation':
        return ('X', Rotation, +1)
    elif word == 'Yrotation':
        return ('Y', Rotation, +1)
    elif word == 'Zrotation':
        return ('Z', Rotation, +1)
    elif word == 'Xposition':
        return (0, Location, +1)
    elif word == 'Yposition':
        return (1, Location, +1)
    elif word == 'Zposition':
        return (2, Location, +1)

def channelZup(word):
    if word == 'Xrotation':
        return ('X', Rotation, +1)
    elif word == 'Yrotation':
        return ('Z', Rotation, +1)
    elif word == 'Zrotation':
        return ('Y', Rotation, -1)
    elif word == 'Xposition':
        return (0, Location, +1)
    elif word == 'Yposition':
        return (2, Location, +1)
    elif word == 'Zposition':
        return (1, Location, -1)

#
#    addFrame(words, frame, nodes, pbones, scale):
#

def addFrame(words, frame, nodes, pbones, scale):
    m = 0
    for node in nodes:
        name = node.name
        try:
            pb = pbones[name]
        except:
            pb = None
        if pb:
            for (mode, indices) in node.channels:
                if mode == Location:
                    vec = Vector((0,0,0))
                    for (index, sign) in indices:
                        vec[index] = sign*float(words[m])
                        m += 1
                    pb.location = (scale * vec  - node.head) * node.inverse
                    for n in range(3):
                        pb.keyframe_insert('location', index=n, frame=frame, group=name)
                elif mode == Rotation:
                    mats = []
                    for (axis, sign) in indices:
                        angle = sign*float(words[m])*Deg2Rad
                        mats.append(Matrix.Rotation(angle, 3, axis))
                        m += 1
                    mat = node.inverse * mats[0] * mats[1] * mats[2] * node.matrix
                    pb.rotation_quaternion = mat.to_quaternion()
                    for n in range(4):
                        pb.keyframe_insert('rotation_quaternion',
                                           index=n, frame=frame, group=name)
    return

#
#    initSceneProperties(scn):
#

def initSceneProperties(scn):
    bpy.types.Scene.MyBvhRot90 = bpy.props.BoolProperty(
        name="Rotate 90 degrees", 
        description="Rotate the armature to make Z point up")
    scn['MyBvhRot90'] = True

    bpy.types.Scene.MyBvhScale = bpy.props.FloatProperty(
        name="Scale", 
        default = 1.0,
        min = 0.01,
        max = 100)
    scn['MyBvhScale'] = 1.0

initSceneProperties(bpy.context.scene)

#
#    class BvhImportPanel(bpy.types.Panel):
#

class BvhImportPanel(bpy.types.Panel):
    bl_label = "BVH import"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"

    def draw(self, context):
        self.layout.prop(context.scene, "MyBvhRot90")
        self.layout.prop(context.scene, "MyBvhScale")
        self.layout.operator("simple_bvh.load")

#
#    class OBJECT_OT_LoadBvhButton(bpy.types.Operator, ImportHelper):
#

class OBJECT_OT_LoadBvhButton(bpy.types.Operator, ImportHelper):
    bl_idname = "simple_bvh.load"
    bl_label = "Load BVH file (.bvh)"

    # From ImportHelper. Filter filenames.
    filename_ext = ".bvh"
    filter_glob = bpy.props.StringProperty(default="*.bvh", options={'HIDDEN'})

    filepath = bpy.props.StringProperty(name="File Path", 
        maxlen=1024, default="")

    def execute(self, context):
        import bpy, os
        readBvhFile(context, self.properties.filepath, 
            context.scene.MyBvhRot90, context.scene.MyBvhScale)
        return{'FINISHED'}    

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

#
#    Registration
#

def menu_func(self, context):
    self.layout.operator("simple_bvh.load", text="Simple BVH (.bvh)...")

def register():
    bpy.utils.register_module(__name__)
    bpy.types.INFO_MT_file_import.append(menu_func)

def unregister():
    bpy.utils.unregister_module(__name__)
    bpy.types.INFO_MT_file_import.remove(menu_func)

if __name__ == "__main__":
    try:
        unregister()
    except:
        pass
    register()