Dev:IT/2.5/Py/Scripts/Cookbook/Code snippets/Blender add-ons
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_info
all'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.
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. 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.
#----------------------------------------------------------
# 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.
#----------------------------------------------------------
# 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()