Книга: Введение в написание скриптов на Питоне для Блендера 2.5x. Примеры кода

Многофайловые пакеты

Многофайловые пакеты

Пакеты — это способ структурирования пространства имен модулей Питона, используя "точечную нотацию имен модулей". Например, имя модуля A.B определяет подмодуль с именем B в пакете с именем A. Точно так же как использование модулей спасает авторов различных модулей от необходимости беспокоиться о существовании совпадающих глобальных имен переменных, использование точечной нотации имен модулей спасает авторов многомодульных пакетов от необходимости волноваться о совпадающих именах модулей. За дополнительной информацией о пакетах Питона, пожалуйста, обратитесь к документации на пакеты Питона

Каждый пакет должен содержать файл __init__.py. Этот файл необходим, чтобы заставить Питон относиться к каталогу, как к содержащему пакет, это сделано для предотвращения у каталогов с частоиспользуемым названием, например, string, непреднамеренного сокрытия действительного модуля, которое происходит в дальнейшем пути поиска модулей. В простейшем случае, __init__.py может быть просто пустым файлом, но он также может выполнять код инициализации пакета. В Блендере __init__.py часто содержит пользовательский интерфейс и информацию аддона, в то время как реальная работа делается в других файлах.

В отличие от других скриптов в этой книге, многофайловый пакет не может быть выполнен из текстового редактора. Он должен быть скопирован в место, которое входит в путь поиска в Блендере, например, addons или addons-contrib, см. раздел аддоны Блендера. К счастью, вам не нужно перезагружать весь Блендер для перезагрузки файлов после каждой модификации. Нажатие F8 на клавиатуре перезагружает все активированные аддоны в Блендере.

Простой пример

Этот пакет разнесён на четыре файла. Три из них создают меши: куб, цилиндр и сферу, соответственно. Это файлы автономных скриптов, которые можно выполнять в окне текстового редактора для отладочных целей. Условие (__name__ == "__main__") истинно, если файл был запущен в автономном режиме.

mycube.py

#----------------------------------------------------------
# File mycube.py
#----------------------------------------------------------
import bpy 
def makeMesh(z):
    bpy.ops.mesh.primitive_cube_add(location=(0,0,z))
    return bpy.context.object  
if __name__ == "__main__":
    ob = makeMesh(1)
    print(ob, "created")

mycylinder.py

#----------------------------------------------------------
# File mycylinder.py
#----------------------------------------------------------
import bpy 
def makeMesh(z):
    bpy.ops.mesh.primitive_cylinder_add(location=(0,0,z))
    return bpy.context.object  
if __name__ == "__main__":
    ob = makeMesh(5)
    print(ob, "created")

mysphere.py

#----------------------------------------------------------
# File mysphere.py
#----------------------------------------------------------
import bpy 
def makeMesh(z):
    bpy.ops.mesh.primitive_ico_sphere_add(location=(0,0,z))
    return bpy.context.object  
if __name__ == "__main__":
    ob = makeMesh(3)
    print(ob, "created")

__init__.py

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

# Для поддержки правильной перезагрузки, пробуем обратиться
# к переменной пакета, если она есть, перезагрузить всё
if "bpy" in locals():
    import imp imp.reload(mycube)
    imp.reload(mysphere)
    imp.reload(mycylinder)
    print("Reloaded multifiles")
else:
    from . import mycube, mysphere, mycylinder
    print("Imported multifiles")

Этот код работает следующим образом.

• Если __init__.py() запускается в первый раз, т.е. при запуске Блендера с включенным аддоном в вашем файле default.blend, "bpy" in locals() возвращает Ложь. Другие файлы в пакете импортируются и в терминале выводится "Imported multifiles".

• Если __init__.py() запускается в первый раз после запуска Блендера с выключенным аддоном в вашем файле default.blend, и вы нажали включение аддона, "bpy" in locals() возвращает Ложь. Другие файлы в пакете импортируются и в терминале выводится "Imported multifiles".

• После того, как дополнения включены, в любое время вы нажимаете F8, чтобы перезагрузить аддоны, "bpy" in locals() возвращает Истину. Другие файлы в пакете перезагружаются, а в терминал выводится "Reloaded multifiles".

#----------------------------------------------------------
# File __init__.py
#----------------------------------------------------------  
# Addon info
bl_info = {
    "name": "Multifile",
    'author': 'Thomas Larsson',
    "location": "View3D > UI panel > Add meshes",
    "category": "3D View"
    } 
# Для поддержки правильной перезагрузки, пробуем обратиться
# к переменной пакета, если она есть, перезагрузить всё
if "bpy" in locals():
    import imp imp.reload(mycube)
    imp.reload(mysphere)
    imp.reload(mycylinder)
    print("Reloaded multifiles")
else:
    from . import mycube, mysphere, mycylinder
    print("Imported multifiles")  
import bpy
from bpy.props import *  
#
# class AddMeshPanel(bpy.types.Panel):
#
class AddMeshPanel(bpy.types.Panel):
    bl_label = "Add meshes"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI" 
    def draw(self, context):
    self.layout.operator("multifile.add",
        text="Add cube").mesh = "cube"
    self.layout.operator("multifile.add",
        text="Add cylinder").mesh = "cylinder"
    self.layout.operator("multifile.add",
        text="Add sphere").mesh = "sphere"  
#
# class OBJECT_OT_AddButton(bpy.types.Operator):
#
class OBJECT_OT_AddButton(bpy.types.Operator):
    bl_idname = "multifile.add"
    bl_label = "Add"
    mesh = bpy.props.StringProperty()  
def execute(self, context):
    if self.mesh == "cube":
        mycube.makeMesh(-8)
    elif self.mesh == "cylinder":
        mycylinder.makeMesh(-5)
    elif self.mesh == "sphere":
        mysphere.makeMesh(-2)
    return{'FINISHED'}  
#
# Регистрация

def register():
    bpy.utils.register_module(__name__)  
def unregister():
    bpy.utils.unregister_module(__name__)  
if __name__ == "__main__":
    register()

Простой импортёр и экспортёр obj-файлов

Формат OBJ часто используется для обмена данными меша между различными приложениями. Первоначально изобретеный для Wavefront Maya, он стал отраслевым стандартом. Это простой ASCII-формат, который содержит строки следующего вида:

v x y z

   Координаты вершин как (x, y, z)

vt u v

   Текстурные координаты как (u, v)

f v1 v2 ... vn

   Грань с n углами, в вершинах v1, v2, ... vn. Для мешей без координат UV.

f v1/vt1 v2/vt2 ... vn/vtn

   Грани с n углами. Углы — это вершины v1, v2, ... vn в 3D-пространстве и vt1, vt2, ... vtn в текстурном пространстве.

Больше конструкций, например, для настройки материала или групп граней, имеются в полноценном экспортёре-импортёре OBJ-формата.

Есть две вещи, которые надо принять во внимание. Во-первых, большинство приложений (насколько мне известно, все, кроме Блендера) используют соглашение, что ось Y указывает вверх, в то время как Блендер использует ось Z для направления вверх. Во-вторых, Майя начинает подсчет вершин с 1, тогда как Блендер начинает отсчет от 0. Это означает, что углы граней на самом деле расположены в вершинах v1-1, v2-1, ... vn-1 в 3D-пространстве и в vt1-1, vt2-1, ... vtn-1 в пространстве текстур.

Простой экспортёр-импортёр OBJ-файлов — это пакет Питона, который состоит из трех файлов: два файла, которые фактически выполняют работу экспорта/импорта, и __init__.py, который делает каталог пакетом.

Простой экспорт OBJ-файлов

Этот скрипт экспортирует выбранный меш как OBJ-файл.

#----------------------------------------------------------
# File export_simple_obj.py
# Простой OBJ-экспортёр, который записывает только вершины, грани и текстурные вершины
#----------------------------------------------------------
import bpy, os 
def export_simple_obj(filepath, ob, rot90, scale):
    name = os.path.basename(filepath)
    realpath = os.path.realpath(os.path.expanduser(filepath))
    fp = open(realpath, 'w')
    print('Exporting %s' % realpath)
    if not ob or ob.type != 'MESH':
        raise NameError('Cannot export: active object %s is not a mesh.' % ob)
    me = ob.data
    for v in me.vertices:
    x = scale*v.co
    if rot90:
        fp.write("v %.5f %.5f %.5fn" % (x[0], x[2], -x[1]))
    else:
        fp.write("v %.5f %.5f %.5fn" % (x[0], x[1], x[2]))
    if len(me.uv_textures) > 0:
        uvtex = me.uv_textures[0]
        for f in me.faces:
            data = uvtex.data[f.index]
            fp.write("vt %.5f %.5fn" % (data.uv1[0], data.uv1[1]))
            fp.write("vt %.5f %.5fn" % (data.uv2[0], data.uv2[1]))
            fp.write("vt %.5f %.5fn" % (data.uv3[0], data.uv3[1]))
            if len(f.vertices) == 4:
                fp.write("vt %.5f %.5fn" % (data.uv4[0], data.uv4[1]))
        vt = 1
        for f in me.faces:
            vs = f.vertices
            fp.write("f %d/%d %d/%d %d/%d" % (vs[0]+1, vt, vs[1]+1, vt+1, vs[2]+1, vt+2))
            vt += 3
            if len(f.vertices) == 4:
                fp.write(" %d/%dn" % (vs[3]+1, vt))
                vt += 1
            else:
                fp.write("n")
    else:
        for f in me.faces:
            vs = f.vertices
            fp.write("f %d %d %d" % (vs[0]+1, vs[1]+1, vs[2]+1))
            if len(f.vertices) == 4:
                fp.write(" %dn" % (vs[3]+1))
            else:
                fp.write("n")
    print('%s successfully exported' % realpath)
    fp.close()
    return

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

Этот скрипт импорта — компаньон предыдущего. Он, конечно, также может использоваться для импорта OBJ-файлов из других приложений.

#----------------------------------------------------------
# File import_simple_obj.py
# Простой OBJ-импортёр, который читает только вершины, грани и текстурные вершины
#----------------------------------------------------------
import bpy, os 
def import_simple_obj(filepath, rot90, scale):
    name = os.path.basename(filepath)
    realpath = os.path.realpath(os.path.expanduser(filepath))
    fp = open(realpath, 'rU') # Universal read
    print('Importing %s' % realpath)
    verts = []
    faces = []
    texverts = []
    texfaces = []
    for line in fp:
        words = line.split()
        if len(words) == 0:
            pass
        elif words[0] == 'v':
            (x,y,z) = (float(words[1]), float(words[2]), float(words[3]))
            if rot90:
                verts.append( (scale*x, -scale*z, scale*y) )
            else:
                verts.append( (scale*x, scale*y, scale*z) )
        elif words[0] == 'vt':
            texverts.append( (float(words[1]), float(words[2])) )
        elif words[0] == 'f':
            (f,tf) = parseFace(words)
            faces.append(f)
            if tf:
                texfaces.append(tf)
        else:
            pass
    print('%s successfully imported' % realpath)
    fp.close()
    me = bpy.data.meshes.new(name)
    me.from_pydata(verts, [], faces)
    me.update()
    if texverts:
        uvtex = me.uv_textures.new()
        uvtex.name = name
        data = uvtex.data
        for n in range(len(texfaces)):
            tf = texfaces[n]
            data[n].uv1 = texverts[tf[0]]
            data[n].uv2 = texverts[tf[1]]
            data[n].uv3 = texverts[tf[2]]
            if len(tf) == 4:
                data[n].uv4 = texverts[tf[3]]
    scn = bpy.context.scene
    ob = bpy.data.objects.new(name, me)
    scn.objects.link(ob)
    scn.objects.active = ob
    return 
def parseFace(words):
    face = []
    texface = []
    for n in range(1, len(words)):
        li = words[n].split('/')
        face.append( int(li[0])-1 )
        try:
            texface.append( int(li[1])-1 )
        except:
            pass
    return (face, texface)

__init__.py

Этот файл содержит пользовательский интерфейс, то есть два класса, которые создают пункты меню для экспортёра и импортёра. Простой экспортёр вызывается из меню File » Export. Есть две опции: логический выбор, чтобы повернуть меш на 90 градусов (для преобразования между осями Y и Z для направления вверх), и масштаб. Простой импортёр вызывается из меню File » Import. Есть две опции: логический выбор, чтобы повернуть меш на 90 градусов (чтобы ось Z указывала вверх), и масштаб.

__init__.py также содержит словарь bl_info , который преобразует пакет в аддон Блендера, код регистрации, и код для импорта/перезагрузки двух других файлов.

#----------------------------------------------------------
# File __init__.py
#----------------------------------------------------------  
# Информация аддона
bl_info = {
    "name": "Simple OBJ format",
    "author": "Thomas Larsson",
    "location": "File > Import-Export",
    "description": "Simple Wavefront obj import/export. Does meshes and UV coordinates",
    "category": "Import-Export"}  
# Для поддержки правильной перезагрузки, пробуем обратиться
# к переменной пакета, если она есть, перезагрузить всё
if "bpy" in locals():
    import imp
    if 'simple_obj_import' in locals():
        imp.reload(simple_obj_import)
    if 'simple_obj_export' in locals():
        imp.reload(simple_obj_export)  
import bpy
from bpy.props import *
from io_utils import ExportHelper, ImportHelper  
#
# Меню Import

class IMPORT_OT_simple_obj(bpy.types.Operator, ImportHelper):
    bl_idname = "io_import_scene.simple_obj"
    bl_description = 'Import from simple OBJ file format (.obj)'
    bl_label = "Import simple OBJ" bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    filename_ext = ".obj"
    filter_glob = StringProperty(default="*.obj;*.mtl", options={'HIDDEN'})
    filepath = bpy.props.StringProperty(
        name="File Path",
        description="File path used for importing the simple OBJ file",
        maxlen= 1024, default= "")
    rot90 = bpy.props.BoolProperty(
        name = "Rotate 90 degrees",
        description="Rotate mesh to Z up",
        default = True)
    scale = bpy.props.FloatProperty(
        name = "Scale",
        description="Scale mesh",
        default = 0.1, min = 0.001, max = 1000.0)  
    def execute(self, context):
        from . import simple_obj_import
        print("Load", self.properties.filepath)
        simple_obj_import.import_simple_obj(
            self.properties.filepath,
            self.rot90,
            self.scale)
        return {'FINISHED'}  
    def invoke(self, context, event):
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}  
#
# Меню Export

class EXPORT_OT_simple_obj(bpy.types.Operator, ExportHelper):
    bl_idname = "io_export_scene.simple_obj"
    bl_description = 'Export from simple OBJ file format (.obj)'
    bl_label = "Export simple OBJ"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW" 
    # Из ExportHelper. Фильтрация имён файлов.
    filename_ext = ".obj"
    filter_glob = StringProperty(default="*.obj", options={'HIDDEN'})
    filepath = bpy.props.StringProperty(
        name="File Path",
        description="File path used for exporting the simple OBJ file",
        maxlen= 1024, default= "")
    rot90 = bpy.props.BoolProperty(
        name = "Rotate 90 degrees",
        description="Rotate mesh to Y up",
        default = True)
    scale = bpy.props.FloatProperty(
        name = "Scale",
        description="Scale mesh",
        default = 0.1, min = 0.001, max = 1000.0)  
    def execute(self, context):
        print("Load", self.properties.filepath)
        from . import simple_obj_export
        simple_obj_export.export_simple_obj(
            self.properties.filepath,
            context.object,
            self.rot90,
            1.0/self.scale)
 return {'FINISHED'}  
    def invoke(self, context, event):
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}  
#
# Регистрация

def menu_func_import(self, context):
    self.layout.operator(IMPORT_OT_simple_obj.bl_idname, text="Simple OBJ (.obj)...")  
def menu_func_export(self, context):
    self.layout.operator(EXPORT_OT_simple_obj.bl_idname, text="Simple OBJ (.obj)...")  
def register():
    bpy.utils.register_module(__name__)
    bpy.types.INFO_MT_file_import.append(menu_func_import)
    bpy.types.INFO_MT_file_export.append(menu_func_export)  
def unregister():
    bpy.utils.unregister_module(__name__)
    bpy.types.INFO_MT_file_import.remove(menu_func_import)
    bpy.types.INFO_MT_file_export.remove(menu_func_export)  
if __name__ == "__main__":
    register()

Оглавление книги


Генерация: 1.187. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз