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

提供: wiki
< Dev:IT‎ | 2.5/Py/Scripts/Cookbook/Code snippets
2018年6月29日 (金) 04:43時点におけるYamyam (トーク | 投稿記録)による版 (1版 をインポートしました)
(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)
移動先: 案内検索

Blender add-ons

Finora abbiamo preso in considerazione solo script indipendenti, eseguiti all'interno del Text Editor. un utente finale è più conveniente se lo script è un Blender add-ons che può essere abilitato dalle User Preferences. E' anche possibile caricare automaticamente uno script ogni volta che Blender viene avviato.

Per trasformare uno script in un add-ons, devi scriverlo in un modo speciale. Ci deve essere una struttura bl_infoall'inizio del file e le funzioni register e unregister devono essere definite alla fine. Inoltre lo script deve essere salvato nelle directory dove Blender normalmente cerca gli add-ons all'avvio. Queste includono le directory addons e addons-contrib che puoi trovare sotto la directory 2.57/scripts.

Shapekey pinning

Lo script può essere eseguito dal Text Editor window as usual. come al solito, comunque può aanche essere letto come add-ons. Le informazioni sull'add-on sono specificate nel dizionario bl_info all'inizio del file.

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

Il significato di molte voci in questo dizionario sono evidenti

  • name: Nome dell'add-on.
  • author: Nome dell'autore.
  • versione: Versione dello script.
  • blender: Versione di Blender.
  • api: Revisione con cui lo script lavora.
  • location: Dove trovare il pulsante.
  • description: Una descrizione mostrata come tooltip e nella documentazione.
  • warning: Un avviso. Se non è lasciato vuoto, una piccola icona sarà mostrata nelle preference utente.
  • wiki_url: Link alla pagina dello script presente nella wiki. In realtà dovrebbe puntare al sito di Blender, ma qui puntiamo a un thread su blenderaertist.org.
  • tracker_url: Link al bug tracker dello script.
  • support: Ufficiale o comunità
  • category: Categoria dello script, per es. 3D View, Import-Export, Add Mesh, o Rigging. Corrisponde alle categorie nelle User Preferences.

Molte di queste voci possono essere semplicemente omesse, come vedremo negli esempi seguenti.

La seconda richiesta di un add-ons è che devono essere definite le funzioni register() e unregister()che normalmente sono messe alla fine del file. register() normalmente fa una chiamata a bpy.utils.register_module(__name__), che registra tutte le classi definite nel file. Può anche conternere inizializzazioni personalizzate. Questo script dichiara anche una proprietà RNA personalizzata. Come abbiamo visto nella sottosezione Proprietà RNA vs proprietà ID, la dichiarazione è necessaria , perché la proprietà bool, se no, viene vista come intera.

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

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

if __name__ == "__main__":
    register()

Annullare la registrazione è simile alla registrazione. L'ultima linea permette di eseguire lo script in maniera indipendente all'interno del Text Editor. Anche se l'utente finale non eseguirà mai lo script nell'editor, è utile avere questa possibilità mentre si fa il debugging.

Copia il file nella directory dove Blender cerca gli add-ons. Riavvia Blender, apri le User Preferences dal menù File » User Preferencese clicca sulla linguetta Add-ons. Il nostro script può essere trovato in fondo alla sezione 3D View. Code Snippets UserPref.png Si riconosce i campi in bl_info nel dizionario. Abilita lo script premendo il checkbox nell'angolo superiore destro. Se vuoi caricare automaticamente lo script, ogni volta che avvii Blender, premi Save As Default nella parte inferiore sinistra della finestra.

Dopo che l'Add-on è stato abilitato, appare nel panello dell'interfaccia. Code Snippets Addon-enabled.png Lo script stesso mostra le shapekeys dell'oggetto attivo nell'interfaccia. Il cubo di default non ha nessun shapekeys. Invece importiamo un personaggio MakeHuman che ha un sacco di espressioni facciali che sono implementate come shapekeys. MakeHuman è un'applicazione che permette di creare facilmente un personaggio. Un personaggio completamente texturizzato e riggato, che può essere esportato in Blender usando il formato MHX (MakeHuman eXchange). I files MHX possono essere importati in Blender con l'importer MHX, che è un add-on distribuito assieme a Blender.

Quello che è importante per questo esempio, è che la mesh di MakeHuman ha un sacco di shapekeys. Se premi il pulsante Pin a destra del valore dello shapekey, la shapekey è pinned, per es. il suo valore è impostato a uno, mentre i valori di tutti gli altri shapekeys sono impostati a zero. Se il pulsante Autokey nella timeline è attivo, un key è impostato per il valore della shapekey. Inoltre se Key all è selezionato sono impostate delle key per ogni shapekey nella mesh. Code Snippets Shapepin.png

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

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

Un semplice importer BVH

Il formato BVH è comunemente usato per trasferire le animazioni dei personaggi, per es. i dati di mocap. Questo programma è un semplice importer BVHThis program is a simple BVH importer, che costruisce uno skeleton con le azioni descritte nel file BVH. E' implementato come add-on con un dizionario bl_info all'inizio del file e il codice per la registrazione alla fine.

Una volta che il sottostante script è stato eseguito, o attivato come add-on, il semplice BVH importer può essere richiamato dal pannello dell'interfaccia utente (CtrlN). Ci sono 2 opzioni: una scelta booleana per ruotare la mesh di 90 gradi (per far puntare l'asse Z in alto) a un fattore di scala.

Questo programma illustra anche come richiamare il file selector premendo un pulsante nel pannello. La classe del pulsante Load BVH eredita sia dalla classe base bpy.types.Operator che da ImportHelper.

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

La classe ImportHelper (probabilmente non documentata) definisce alcuni attributi che sono usati per filtrare i file visibili nel file selector.

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

C'è un'analoga classe ExportHelper che limita le scelte disponibili in un 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, 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 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()