10 сентября 2011

Blender 2.59 и его новое python api для экспорта

Привет. У меня есть небольшая статья по экспорту моделей из Blender. И скрипт прекрасно работает для версии 2.49, но в ветке 2.5 поменяли api, и он перестал работать. Печально? Да, но это легко поправимо.
Скачем с сайта www.blender.org докупентацию к python api которая содержит всего 1389 страниц, и я надеюсь что со временем она станет больше (я абсолютно серьезно, описание функций и классов не балует нас подробной информацией, а хотелось бы). Благо еще есть и наборы готовых модулей и скриптов в /usr/share/blender/{version}/scripts, или где у вас установлен blender. Можно так же воспользоваться версией документации с сайта.
Для начала давайте просто откроем окно с выбором файла. Теперь нет простой функции которая может это сделать. Нужно наследовать класс bpy.types.Operator. К счастью, для удобства написания своего экспорта есть класс ExportHelper, благодаря которому не сложно создать достойного наследника для bpy.types.Operator. Он привнесет в нашу жизнь еще немного этой замечательной магии ООП которую я так ненавижу. К классу отсутствует хоть какое либо описание, но найдя его в скриптах (/usr/share/blender/2.59/scripts/modules/bpy_extras/io_utils.py), можно понять, что ничего кроме как проверки расширения файла и его подстановки, в случае необходимости, скрипт не делает. Есть еще проверка настроек преобразования осей (axis_conversion_ensure), но я не уверен что понимаю что это и как это работает. Вернемся же к нашим баранам, если не заглядывать в ExportHelper, то можно не узнать, что для того, что бы указать расширение, у нас должен быть атрибут filename_ext в дочернем классе.
В остальном документация более разговорчива и снабжена примерами. К тому же вам может помочь вот эта книга. Называется она Programming Add-ons for Blender 2.5 и узнал я о ней из этой новости.
Вот так было раньше:
for face in mesh.faces:
    for vert in face.verts:
        for co in vert.co:
            vertices.append(co)
        for no in vert.no:
            normals.append(no)
    for co in face.uv:
        uv.append(co[0])
        uv.append(-co[1])
Теперь, несмотря на всю ООП магию, MeshFace - класс поверхности, не содержит uv координат и вершин, есть только индексы вершин и индекс uv поверхностей (они совпадают с индексами самих поверхностей). Надеюсь в терминологии я ничего не напутал. Теперь это так выглядит:
for face in mesh.faces.values():
    for ind in face.vertices:
        for co in mesh.vertices[ind].co:
            vertices.append(co)
        for no in mesh.vertices[ind].normal:
            normals.append(no)
    for uv_co in mesh.uv_textures.active.data[face.index].uv:
        uv.append(uv_co[0])
        uv.append(-uv_co[1])
Перейдем к остальному и рассмотрим весь пример скрипта снабженного комментариями. Скрипт претерпел изменение в связи с тем, что прошлый был написан каким-то неадекватным названием романа Достоевского, и как это не печально, похоже это был я. Думаю к названию переменных надо было отнестись более аккуратно что ли. Вот что получилось:
import bpy
from bpy_extras.io_utils import (ExportHelper)

import os

class ExportInH(bpy.types.Operator, ExportHelper):
    """Export to header file for including in C program"""
    bl_idname = "export.in_h"
    bl_label = "Export in *.h (Header File)"

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

    def write_array(self, f, name, array, step=6):
        """
        Метод который пишет массив в стиле исходников Си.
 
        Добавлен перенос строк для лучшего чтения внутри файла.
        """
        f.write('GLfloat {0}[] = {{\n'.format(name))
        s = 0;
        for el in array:
            s += 1
            f.write('{0: f},'.format(el))
            if s >= step:
                s = 0
                f.write('\n')
            else:
                f.write(' ')
        f.write('};\n\n')

    def write_h(self, mesh, vertices, normals, uv):
        """
        Пишем header файл с массивом вершин, нормалей и uv координатами.
        """
        filepath = os.path.dirname(self.filepath)
        name = '.'.join(os.path.basename(self.filepath).split('.')[0:-1])

        out = open(filepath + "/" + name + ".h", 'w')
        out.write("#ifndef {0}_H__\n".format(name.upper()))
        out.write("#define {0}_H__\n".format(name.upper()))

        out.write("\n#include <GL/gl.h>\n")

        out.write('GLint {0}_size = {1};\n\n'.format(name,len(mesh.faces)*3))

        self.write_array(out, name + '_vertices', vertices)
        self.write_array(out, name + '_normals', normals)
        self.write_array(out, name + '_uv', uv,)

        out.write('''void
{0}_draw()
{{
  glVertexPointer(3, GL_FLOAT, 0, {0}_vertices);
  glNormalPointer(GL_FLOAT, 0, {0}_normals);
  glTexCoordPointer(2, GL_FLOAT, 0, {0}_uv);
 
  glDrawArrays(GL_TRIANGLES, 0, {0}_size);
}}
 
'''.format(name))

        out.write("#endif /* {0}_H__ */".format(name.upper()))
        out.close()

    def execute(self, context):
        """
        Переопределенный метод выполняемый после выбора файла.
        """
        scene = context.scene
        select_object = context.object
        # Копия объекта нужна, что бы превратить квадратные полигоны в
        # треугольники средствами самого blender'а.
        copy_object = select_object.copy()
        copy_object.data = select_object.data.copy()
        context.scene.objects.link(copy_object)

        # Для применения операции "квадрат в треугольник" нужно сделать новый
        # объект активным.
        copy_object.select = True
        context.scene.objects.active = copy_object

        # Сделать данную операцию можно только в режиме редактирования.
        mode = context.mode
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.quads_convert_to_tris()
        bpy.ops.object.mode_set(mode=mode)

        mesh = copy_object.to_mesh(scene, False, 'PREVIEW')

        #{{{export this
        vertices = list()
        normals  = list()
        uv       = list()

        for face in mesh.faces.values():
            for ind in face.vertices:
                for co in mesh.vertices[ind].co:
                    vertices.append(co)
                for no in mesh.vertices[ind].normal:
                    normals.append(no)
            for uv_co in mesh.uv_textures.active.data[face.index].uv:
                uv.append(uv_co[0])
                uv.append(-uv_co[1])

        self.write_h(mesh, vertices, normals, uv)

        #}}}export this

        # И конечно не забываем убрать копию объекта
        context.scene.objects.unlink(copy_object)

        select_object.select = True
        context.scene.objects.active = select_object
        return {'FINISHED'}

# Добавляем наш экспорт в меню.
def menu_func(self, context):
    self.layout.operator_context = 'INVOKE_DEFAULT'
    self.layout.operator(ExportInH.bl_idname, text="Export in *.h (Header File)")

# Регистрируем наш класс
def register():
    bpy.utils.register_class(ExportInH)
    bpy.types.INFO_MT_file_export.append(menu_func)

# Тоже что и выше, но наоборот
def unregister():
    bpy.utils.unregister_class(ExportInH)
    bpy.types.INFO_MT_file_export.remove(menu_func)

# Исключительно для тестирования скрипта.
if __name__ == '__main__':
    register()
    bpy.ops.export.in_h('INVOKE_DEFAULT')
Вот и все собственно. Если вы подумали что новое api мне не понравилось, то боюсь вас расстроить, оно мне вполне приглянулось. Можно скачать все одним архивом (md5sum: 44d2659fdb203ba5df88a5a22e426847) с моделью, скриптом и программой показывающей экспортируемую модель. Спасибо что дочитали/проскролили до этого момента. Пока.