Аддоны Блендера

До сих пор мы рассматривали только автономные скрипты, которые выполняются из окна текстового редактора. Для конечных пользователей более удобно, если скрипт — это аддон (add-on, надстройка) Блендера, который может быть включен в окне Пользовательских настроек. Также можно автоматически загружать скрипт каждый раз при запуске Блендера

Для того, чтобы скрипт был аддоном, он должен быть написан по-особому. Там должна быть структура

bl_info
в начале файла, а также в конце должны быть определены функции
register
(регистрации) и
unregister
(отмены регистрации). Кроме того, скрипт должен быть размещен в месте, в котором Блендер ищет аддоны при запуске. Оно включает в себя каталоги
addons
и
addons-contrib
, которые расположены в подкаталоге
2.57/scripts
каталога, в котором находится Блендер.


Прикрепление ключей формы

Этот скрипт может быть выполнен, как обычно, из окна текстового редактора. Тем не менее, он также может быть доступен как аддон Блендера. Информация аддона указывается в словаре

bl_info
в начале файла.

bl_info = {

   'name': 'Shapekey pinning',

   'author': 'Thomas Larsson',

   'version': (0, 1, 2),

   'blender': (2, 5, 7),

   'api': 35774,

   "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"}


Смысл большинства ключей в этом словаре очевиден.

name
: Название аддона.

author
: Имя автора.

version
: Версия скрипта.

blender
: Версия Блендера.

api
: Номер ревизии, с которой скрипт работает.

location
: Где искать кнопки.

description
: Описание, отображаемое в виде всплывающей подсказки и в документации.

warning
: Предупреждающее сообщение. Если не пусто, в окне пользовательских настроек будет отображаться небольшой предупреждающий знак.

wiki_url
: Ссылка на вики-страницу скрипта. Должна быть реальным Блендер-сайтом, но здесь мы ссылаемся на тему в форуме blenderartists.org.

tracker_url
: Ссылка на трекер ошибок скрипта.

support
: Официальная поддержка или сообщество

category
: Категория скрипта, т.е. 3D View, Import-Export, Add Mesh, или Rigging. Соответствует категориям в окне Пользовательских настроек.


Многие элементы могут быть просто опущены, как мы увидим в других примерах ниже.


Второе требование к аддону — это определение функций

register()
и
unregister()
которые обычно располагаются в конце файла.
register()
обычно вызывает оператор
bpy.utils.register_module(__name__)
, в котором регистрируются все классы, определенные в файле. Она также может содержать несколько пользовательских задач инициализации. Скрипт этого примера также объявляет пользовательские RNA-свойства. Как мы видели в разделе RNA-свойства против ID-свойств, объявление необходимо здесь потому, что в противном случае логическое свойство будет отображаться как целое число.


def register():

   initialize()

   bpy.utils.register_module(__name__)  


def unregister():

   bpy.utils.unregister_module(__name__)


if __name__ == "__main__":

   register()


Отмена регистрации аналогична регистрации. Последние строки делают возможным запуск скрипта в автономном режиме из окна Текстового редактора. Даже если пользователь никогда не будет выполнять скрипт из редактора, полезно иметь такую ​​возможность при отладке.

Скопируйте файл в место, где Блендер ищет аддоны. Перезагрузите Блендер. Откройте окно Пользовательских настроек из меню File » User Preferences, и перейдите во вкладку Add-ons. Наш скрипт можно найти в нижней части раздела 3D View.



Мы узнаём поля из словаря

bl_info
. Включите скрипт, нажав флажок в правом верхнем углу. Если вы хотите, чтобы аддон загружался каждый раз при запуске Блендера, нажмите кнопку Save As Default в нижней части окна.

После включения аддона, он появляется в UI-панели.



Сам скрипт отображает ключи формы активного объекта на панели интерфейса. Куб по умолчанию не имеет ключей формы. Вместо него мы импортируем персонаж MakeHuman, имеющий множество выражений лица, которые реализуются через ключи формы. MakeHuman - это приложение, которое легко позволяет вам конструировать персонаж. Полностью оснащённый и текстурированный персонаж может быть экспортирован в Блендер использованием формата MHX (MakeHuman eXchange). MHX-файлы могут быть импортированы в Блендер с помощью импортера MHX, аддона, который распространяется с Блендером.

Что имеет значение для настоящего примера, это что меш MakeHuman имеет множество ключей формы. Если вы нажмёте кнопку Pin (прикрепить) справа от значения ключа формы, ключ формы будет закреплен, то есть его значение станет равным единице, в то время как значения всех остальных ключей формы будут равны нулю. Если кнопка Autokey на временной шкале нажата, будет добавлен ключ на значение ключа формы. Если к тому же включена опция Key all, ключи добавляются для каждого ключа формы меша.



#---------------------------------------------------------- 

# File shapekey_pin.py 

#---------------------------------------------------------- 

bl_info = {

   'name': 'Shapekey pinning',

   'author': 'Thomas Larsson',

   'version': '(0, 1, 2)',

   'blender': (2, 5, 7),

   "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 


# 

# инициализация и регистрация

#  


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()


Простой импорт BVH-файлов

BVH формат обычно используется для передачи анимации персонажей, например, от данных захвата движения (mocap). Эта программа простого импортера BVH, которая строит скелет с действием (action), описанный в файле BVH. Он реализован в виде аддона Блендера со словарём

bl_info
в верхней части файла и кодом регистрации в конце.

После выполнения скрипта или включения его в качестве аддона, простой импортер BVH может быть вызван из панели пользовательского интерфейса (Ctrl+N). Есть две опции: логическая переменная с информацией о том, повернуть ли меш на 90 градусов (чтобы направить Z вверх), и масштаб.

Эта программа также показывает, как вызвать диалог выбора файлов, нажав кнопку на панели. Класс кнопки Load BVH наследуется от двух базовых классов

bpy.types.Operator
и
ImportHelper
.

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

Класс

ImportHelper
(возможно, недокументированный) определяет некоторые атрибуты, которые используются для фильтрации файлов, отображающихся в диалоге выбора файлов.

filename_ext = ".bvh" 

filter_glob = bpy.props.StringProperty(default="*.bvh", options={'HIDDEN'}) 

filepath = bpy.props.StringProperty(name="File Path",

   maxlen=1024, default="")

Существует аналогичный класс ExportHelper, который ограничивает имеющийся выбор файлов экспорта.



#---------------------------------------------------------- 

# File simple_bvh_import.py 

# Simple bvh importer 

#---------------------------------------------------------- 


bl_info = {

   'name': 'Simple BVH importer (.bvh)',

   'author': 'Thomas Larsson',

   'version': (1, 0, 0),

   'blender': (2, 5, 7),

   'api': 34786,

   'location': "File > Import",

   'description': 'Simple import of Biovision bvh',

   'category': 'Import-Export'}  


import bpy, os, math, mathutils, time 

from mathutils import Vector, Matrix 

from 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_LoadBvhButt
on(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()



Загрузка...