Данная статья является моими собственными исследования в использовании XNA и построении для нее контента за последние 5-6 месяцев.
Что послужило исследованием связки Blender + Xna? Дело в том, что я и еще несколько человек решили написать небольшую игру (кто бы сомневался :) ). Но мы не смогли нормально работать вместе из-за того, что всем, в том числе и моделерам необходимо было ставить на компьютеры Visual C# и XNA GS для того, чтобы добавлять в игру контент самостоятельно. В итоге мы пришли к выводу, что необходимо написать экспортер моделей из какого-либо пакета моделирования. В итоге наш взгляд упал на Blender. Есть две причины, по которым мы его выбрали: 1) его бесплатность, 2) т.к. в Blender есть собственный игровой движок, то есть некоторые средства, которые могли бы помочь в использовании Blender в качестве редактора уровней (об этих средствах мы tit поговорим в самой статье). Мне понадобился примерно месяц с небольшим на изучение языка Python, т.к. Blender в качестве языка скриптов использует именно его, изучение архитектуры API Blender’а и создание собственного экспортера сцены.
Изначально я планировал написать пример загрузки сцены с использованием только XNA. Но по самой XNA существует достаточно много материала, и я решил, что статья получится немного скучной. Поэтому я решил в качестве графического API использовать Xen, который в свою очередь реализован средствами XNA. Почему именно Xen? Вот его основные возможности:
Полная замена системы эффектов с интеграцией кода и проверкой.
Для вершинных и индексных буферов нет необходимости в создании декларативной части структуры вершины.
Быстрое управление состоянием визуализации.
Легкие в реализации цели визуализации (прям каламбур какой-то :) )
Гибкая игровая логическая система обновления, включая асинхронные обновления (многопоточность).
Высокопроизводительный и гибкий формат анимированных моделей.
Система материалов, поддерживающая до 10 источников света за проход, нормалмэппинг, инстансинг и др.
Мощная 2D/3D система частиц, полностью программируемая используя схему валидации XML. С поддержкой ускорения на GPU.
Большие возможности по получении статистики начиная от количества визуализированных треугольников и заканчивая использованием потоков на XBOX
Набор 2D элементов, вывод текста, фильтрация изображений и др.
Xen - графическое API для XNA с открытым исходным кодом.
Не смотря на то, что статья написана для Xen, все, что описано в ней можно реализовать и на XNA, но тогда Вам придется самим разработать много дополнительного кода такого как, например, по вывод на экран с сортировкой по глубине.
К статье прилагаются все исходные коды проекта, скрипт экспорта из Blender, тестовая сцена для экспорта. Все это можно скачать одним архивом по следующей ссылке:
Материалы и Media, использованные в статье:
В качестве документации по Xen были использованы примеры, входящие в его состав (в состав Xen входит 25 примеров с подробным описанием на английском языке). Для бонуса к статье (реализация SkyBox на Xen) использовалась медиа из примера реализации воды, также за основу была взята реализация скайбокса Модель перекрестка была создана моим другом Akima.
Данная статься, можно сказать, является не только описанием загрузки сцены, в статье так же рассматривается создание графа сцены и др. составляющих игрового движка. Альтернативную реализацию графа сцены и др. составляющих движка можно посмотреть в альтернативной реализации Дмитрия Тимофеева (General) – движка gEngine
Итак, экспорт сцены из Blender.
Прежде всего, немного теории.
Blender – бесплатный пакет трехмерного моделирования. Возможностей последней стабильной версии хватает для создания таких мультфильмов как Elephants Dream, Big Buck Bunny и др. Как и все платные пакеты 3D моделирования, такие как 3DS MAX или Maya, Blender имеет возможность расширения своих функций, путем создания скриптов на языке Python. Чтобы эффективно писать скрипты для Blender, необходимо знать, откуда взять информацию о Python – API функциях , которые можно применять в его скриптах.
Каждый объект сцены в Blender является наследником общего класса Blender.Object и имеет общие свойства и методы, которые характерны для всех трехмерных объектов. Для экспорта интересны такие свойства как имя объекта, тип объекта и его трансформации. После получения общих сведений об объекте необходимо получить данные, специфичные для каждого типа объектов в Blender. Такими объектами могут быть Empty, Mesh, Lamp, Camera, Armature, Curve и другие. Нам пока интересны первые четыре типа объектов, из которых особенно интересны Empty, Mesh.
Краткое описание экспортируемых объектов:
Empty – объект пустышка, имеющий только имя и трансформации и может использоваться для расположения ключевых мест в игре, таких как области входа и выхода из уровня, расположения геометрии, общей для всех уровней, например камни, деревья и другие объекты, которые находятся в общем архиве игры. Как это сделать, я расскажу чуть позже. Mesh – объект, специальными данными которого является информация о геометрии. Каждая геометрия имеет набор связанных с ней вершин (Vertex) и граней (Face), причем каждая грянь может быть как треугольником, так и квадом (экспорт таких граней будет не сложнее чем обычных треугольников, об этом Вы узнаете ниже). Lamp - источник света. В Blender имеются следующие типы источников света: Lamp, Sun, Spot, Hemi, Area, Photon. Lamp – точечный, Sun – направленный, Spot – конусный и т.д. В данной статье я расскажу как экспортировать первые два типа, источников света. Если вам понадобятся остальные, то их экспорт можно будет выполнить так же просто. Camera – тут в принципе и рассказывать много не надо. Камера – объект, отвечающий за место и направление взгляда в виртуальном мире. Нам этот объект пригодится для создания анимации полета по уровню или, например, для расположения фиксированных точек взгляда на сцену (например, как в квестах, и некоторых других жанрах игр).
Я сказал анимации полета? Ну конечно! Каждый объект Blender’а можно анимировать, это же мощный пакет 3D анимации! Конечно, я не буду разбирать в этой статье, как экспортировать скелетную анимацию, но простую анимацию объектов, такую как перемещение, масштабирование и вращение мы сможем экспортировать без особых усилий. Экспорт анимации будет выполнен в исходном виде, т.е. так, как это сохранено в Blender.
Кроме того, каждый объект может иметь дополнительные параметры. Должен сказать, что этот фактор стал решающим, при выборе Blender в качестве источника игрового контента, хотя вторым фактором была его бесплатность. Это значит, что нет необходимости разрабатывать дополнительные редакторы, создающие логику отдельных объектов. Достаточно добавить нужную информацию объекту о его логике и обработать эту информацию при загрузке сцены. Наличие такого инструмента в Blender обусловлено встроенным игровым движком, но производительность такого движка на много ниже чем скорость индивидуального движка, разработанного под свои нужды. Поэтому под каждый проект чаще всего приходится создавать отдельное решение. В нашем случае XNA дает твердый фундамент для этих целей.
Теперь перейдем к практике.
Первым делом необходимо скачать Blender, а для работы скриптов Python (какую версию Python необходимо скачать для текущей версии Blender, показано под ссылкой на закачку). На момент написания статьи была возможность закачки Blender v2.49 и Python SDK 2.6.3. Я не буду описывать установку этих продуктов, с этим Вы справитесь сами. Но последовательность работы с Blender я постараюсь описать с применением скриншотов так, что бы Вам не заблудиться в его интерфейсе.
Запустив Blender, первым делом необходимо перейти в режим написания скриптов (на следующем рисунке пятый пункт выпадающего списка режимов «5 – Scripting»). На самом деле каждый режим представляет собой набор окон, расположенных для выполнения определенных задач. Каждый режим можно перестроить по своему, а так же можно добавить необходимое количество собственных режимов расположения окон.
В режиме редактирования скриптов Blender изменит расположение своих окон как показано на следующем рисунке:
Режим написания скриптов состоит из трех окон. Левое верхнее – 3D сцена, левое нижнее – привязка скриптов «ScriptLinks» и наконец, правое – текстовый редактор с возможностью подсветки синтаксиса и автозавершения (правда во многих случаях она не работает из-за того, что Python не строго типизированный язык). Подсветка синтаксиса включается нажатием на специальные кнопки – флажки в панели инструментов данного окна. На следующем рисунке нажаты три из них для обеспечения более комфортного скриптописания.
Далее необходимо создать новый скрипт или загрузить его из файла. Я думаю многие, читающие эту статью, выполнят второе действие, скачав готовый скрипт экспорта (ссылка на него находится в начале данной статьи).
Открытый файл скрипта в редакторе API Blender’а будет выглядеть следующим образом.
Пишем скрипт экспорта
В данной части статьи я хотел бы подробно описать порядок создания скрипта и применяемые при этом API Blender’а. Но сначала несколько слов о языке Python.
Надо сказать, что Python первый язык из тех, которые я изучал, не содержит операторных скобок (фигурные скобки в C#, Begin End в паскале и делфи, на бэйсике каждый блочный оператор имеет завершающее ключевое слово и т.д.). В качестве группировки операторов используется отступ от левого края документа, причем различается отступ как пробелов, так и табуляции. Это значит, что если Вы не следите за тем, что используете в качестве отступа и скрипт вроде написан правильно, при запуске, интерпретатор языка может выдать кучу ошибок, связанных с неправильным отступом. В литературе в качестве отступа рекомендуется использовать пробелы, а не табуляцию.
Так же в языке присутствуют такие элементы как списки, кортежи и словари. Думаю первое и третье понятно, а второе – это аналог списка, только не позволяющий изменять его элементы. Все три объекта создаются с помощью скобок [ ], ( ) и { } соответственно. Для того, чтобы отличить кортеж с одним элементом от математического выражения ставится запятая после первого элемента, а второй не добавляется т.е. например (1,) а не (1). Каждый из этих трех объектов поддерживает так называемые срезы. Срез – это тоже самое, что и Subctring для строк, т.е. – средство получения подмножеств. Записывается как [начало : конец]. Ну да ладно о питоне, что-то заговорился я. Если вы захотите подробно изучить питон, то сами найдете достаточно литературы по нему. В справочной системе API Blender’а содержится также достаточное количество примеров. А сейчас давайте перейдем к программированию.
Я надеюсь, что скрипт будет иметь правильные отступы после конвертации статьи в формат для сайта. Если нет, то смотрите лучше код из исходника. Комментарии я в коде писать не буду, т.к. текстовый редактор Blender’а не поддерживает русский шрифт, хотя интерфейс Blender’а можно русифицировать. Комментарии я буду писать здесь в коде примера к статье.
Первое, что нам встретится в любом скрипте Blender’а, расширяющем его функции – это заголовок. Заголовок читается самим Blender’ом, а не интерпретатором питона и выполнен в виде многострочного теста, перед которым ставится команда #!BPY. В заголовке указывается название скрипта, версия Blender’а, для которой написан скрипт, группа, определяющая размещение скрипта в меню Blender’а (в нашем случае «Export») и подсказка пункта меню.
Заголовок скрипта
1
2
3
4
5
6
7
#!BPY"""Name: 'XnaDev.Ru Exporter Example'Blender: 248Group: 'Export'Tooltip: 'Exports data from Blender to XNA'"""
Далее импорт пространств имен и объектов пространств имен в текущее пространство имен (пространство имен скрипта)
Прежде всего, оговорюсь о том, как должна быть построена программа на питоне. Т.к. питон – интерпретируемый язык, то необходимо писать все функции перед из использованием. Этими функциями в нашем случае будут функции записи XML-тэгов в текстовый документ. Все функции, записывающие информацию в файл, будут получать первым параметром ссылку на объект файла, который будет содержать методы для записи в файл.
# функция записи в файл отступа от края документаdef wrIndent(file, indent): if indent:for i inxrange(indent): # функция xrange создает виртуальный массив начиная с единицыfile.write(" ")# запись в файл отступа# функция записи открывающего xml тэга с атрибутами, передающимися в виде списка кортежейdef xmlWrOp(file, name, attr = None, indent = 0, iscomplete = 0): wrIndent(file, indent)file.write("<" + name)if attr:for key, value in attr:# Здесь пример форматирования строки. Символы, начинающиеся с % будут последовательно# заменены аргументами, переданными в виде кортежаfile.write(' %s="%s"'%(key, str(value)))if iscomplete:file.write(" /")file.write(">\n")# функция записи в файл завершающего тэгаdef xmlWrCl(file, name, indent = 0): wrIndent(file, indent)file.write("" + name + ">\n")# функция записи в файл конструкции в виде двухмерного вектора с заданным именемdef xmlWriteVector2D(file, name, vals, indent = 0): xmlWrOp(file, name, [("X", round(vals[0], 5)), ("Y", round(vals[1], 5))], indent, True)# функция записи в файл конструкции в виде трехмерного вектора с заданным именем# Обратите внимание, что аргументы вектора сохраняются в порядке 1,2,0 а не 0,1,2# т.к. Blender имеет систему координат, в которой ось Z является верхом# необходимо преобразовать трансформации Blender’а к системе координат XNAdef xmlWriteVector3D(file, name, vals, indent = 0): xmlWrOp(file, name, [("X", round(vals[1], 5)), ("Y", round(vals[2], 5)), ("Z", round(vals[0], 5))], indent, True)# функция записи трансформаций объекта сцены в файлdef xmlWriteTransforms(file, obj, indent = 0): xmlWrOp(file, "Transforms", [], indent)# получаем локальные трансформации объекта# если у объекта нет родителя, то трансформации будут глобальными matrix = obj.getMatrix('localspace') pos = matrix.translationPart() rot = matrix.toEuler() scl = matrix.scalePart()# сохраняем трансформации в файл xmlWriteVector3D(file, "Pos", [pos[0], pos[1], pos[2]], indent + 1) xmlWriteVector3D(file, "Rot", [rot[0], rot[1], rot[2]], indent + 1) xmlWriteVector3D(file, "Scl", [scl[0], scl[1], scl[2]], indent + 1) xmlWrCl(file, "Transforms", indent)# функция записи дополнительных параметров объектов сцены в файлdef xmlWrProperties(file, props, indent = 0): xmlWrOp(file, "Properties", [], indent)if props:for prop in props: xmlWrOp(file, "Property", [("Name", prop.getName()),("Value", prop.getData())], indent+1, 1) xmlWrCl(file, "Properties", indent)# функция переводящая градусы в радианыdef toRadian(value):return value *math.pi / 180.0# функция перевода значения lens, используемого в Blender для задания размера линз камеры# в FOV, для использования при создании камеры в XNAdef convertLens(lens):return2*math.atan(16 / lens)# коллекция типов источников светаlampTypes = ('Lamp', 'Sun', 'Spot', 'Hemi', 'Area', 'Photon')# коллекция типов продолжения кривых анимации на концах промежутков анимацииipoExtendTypes = ("CONST", "EXTRAP", "CYCLIC", "CYCLIC_EXTRAP")# коллекция поддерживаемых экспортером объектов supportedObjects = ('Mesh', 'Empty', 'Lamp', 'Camera')# переменная списка, в которую будут записаны все материалы экспортируемых объектовsceneMaterials = []# коллекции необходимее для определения «правильных» имен кривых при экспорте# основная задача – приведение анимации в координатную систему XNAipoFrom = ('LocX', 'LocY', 'LocZ', 'RotX', 'RotY', 'RotZ', 'ScaleX', 'ScaleY', 'ScaleZ')ipoTo = ('LocZ', 'LocX', 'LocY', 'RotZ', 'RotX', 'RotY', 'ScaleZ', 'ScaleX', 'ScaleY')# функция, преобразующая имя кривой в соответствии с вышеприведенными коллекциямиdef convertIpoName(name):for i, n inenumerate(ipoFrom):if n == name:return ipoTo[i]return name # для кривых, не входящих в коллекцию ничего не меняем# функция, записывающая данные кривой в файл# тут необходимо знать то, что каждая точка кривой состоит из трех 2D векторов h1, p, h2# первый и третий – это манипуляторы, которыми можно управлять формой кривой в редакторе Blender.# p – сама точка кривой.# В координате x вектора хранится значение времени в секундах, в y – значение кривойdef xmlWriteCurve(file, curve, indent = 0): curveName = convertIpoName(curve.name) xmlWrOp(file, curveName, [("Extend", ipoExtendTypes[curve.extend])], indent)for point in curve.bezierPoints: xmlWrOp(file, "Point", [], indent + 1) h1, p, h2 = point.vecif(curveName in["RotX", "RotY", "RotZ"]):# вращение переводим в радианы и умножаем на 10, т.к. Blender # хранит градусы в единицах на порядок меньше h1 = [h1[0], toRadian(h1[1]*10.0)] p = [p[0], toRadian(p[1]*10.0)] h2 = [h2[0], toRadian(h2[1]*10.0)]elif(curveName == "Lens"):# Lens переводим в FOV h1 = [h1[0], convertLens(h1[1])] p = [p[0], convertLens(p[1])] h2 = [h2[0], convertLens(h2[1])] xmlWriteVector2D(file, "h1", h1, indent + 2) xmlWriteVector2D(file, "p", p, indent + 2) xmlWriteVector2D(file, "h2", h2, indent + 2) xmlWrCl(file, "Point", indent + 1) xmlWrCl(file, curveName, indent)# Функция записывающие все кривые анимации объекта в файл# здесь отмечу, что есть два места хранения кривых анимации у каждого объекта# первое место – сам объект, в котором охраняться кривые, общие для всех объектов# второе место – контейнер данных, который различается для каждого типа объектов сцены# я привожу код для экспорта кривых из обоих мест, но реализация анимации не трансформаций# объектов, а других параметров выходит за рамки текущей статьиdef xmlWriteIpo(file, obj, indent = 0): xmlWrOp(file, "Ipo", [], indent)if obj.ipo: # общие кривые анимацииfor curve in obj.ipo.curves: xmlWriteCurve(file, curve, indent + 1)try:for curve in obj.data.ipo: xmlWriteCurve(file, curve, indent + 1)except:passelif obj.data: # контейнер, специфичный для каждого типа объектовtry: ipo = obj.data.ipoif ipo:for curve in ipo: xmlWriteCurve(file, curve, indent+1)except:pass xmlWrCl(file, "Ipo", indent)# функция записи в файл информации о грани геометрииdef xmlWriteFace(file, face, tan, ind, indent = 0): xmlWrOp(file, "Face", [], indent)for i in ind: attribs = [("Index", face.v[i].index)]if face.smooth: # Если грань гладкая, то нормаль берется из вершины attribs = attribs + [("NX", round(face.v[i].no[1], 5)), \("NY", round(face.v[i].no[2], 5)), ("NZ", round(face.v[i].no[0], 5))]else: # иначе нормаль берется из самой грани attribs = attribs + [("NX", round(face.no[1], 5)), ("NY", round(face.no[2], 5)), ("NZ", round(face.no[0], 5))] attribs = attribs + [("TX", round(tan[i][1], 5)), ("TY", round(tan[i][2], 5)), ("TZ", round(tan[i][0], 5))] attribs = attribs + [("UVX", round(face.uv[i][0], 5)), ("UVY", round(face.uv[i][1], 5))] xmlWrOp(file, "V", attribs, indent + 1, True) xmlWrCl(file, "Face", indent)# Функция записи в файл данных о геометрии объекта «Mesh»# т.к. Blender изначально не проектировался как игровой движок, данные геометрии хранятся# несколько иначе, чем в игровых движках или той же XNA. Построение нужных буферов # мы будем выполнять при загрузке сцены.# Массив вершин содержит только положение и нормаль, а для каждой грани можно получить текстурные координаты,# тангенсы и нормаль, если грань не сглаженная. В каждой грани содержатся ссылки на вершины,# на основе которых она построена. # Здесь хочу заметить, что в Blender могут присутствовать как треугольные грани, так и квады,# поэтому, реализация алгоритма сохранения это учитывает.def xmlWriteMeshData(file, mesh, indent = 0): md = mesh.getData(mesh = 1)# данные о геометрии содержатся в контейнере объекта# первое что мы должны проверить – это наличие текстурных координатifnot md.faceUV:print"Mesh '" + mesh.name + "' has no UV, skipped."return xmlWrOp(file, "Mesh", None, indent)# сохраняем массив вершин xmlWrOp(file, "Vertices", [("Count", len(md.verts))], indent + 1)for v in md.verts: xmlWrOp(file, "Vert", [("Index", v.index), \("X", round(v.co[1], 5)), \("Y", round(v.co[2], 5)), \("Z", round(v.co[0], 5))], indent + 2, 1) xmlWrCl(file, "Vertices", indent + 1)# получаем все материалы объекта# если массив материалов объекта не пустой, то сохраняем для каждого материала# набор граней, которым назначен этот материал# если материалов нет, то сохраняем все грани с материалом None# Каждый набор граней сохраняется как MeshPart и атрибутом Material materials = md.materialsif materials:for matIndex inxrange(len(materials)):ifnot materials[matIndex]in sceneMaterials: sceneMaterials.append(materials[matIndex]) xmlWrOp(file, "MeshPart", [("Material", materials[matIndex].name)], indent + 1) tan = md.getTangents()for i, face inenumerate(md.faces):if face.mat == matIndex: xmlWriteFace(file, face, tan[i], (0, 2, 1) , indent + 2)iflen(face.v) == 4: # если грань – квад, то сохраняем второй треугольник xmlWriteFace(file, face, tan[i], (2, 0, 3) , indent + 2) xmlWrCl(file, "MeshPart", indent + 1)else: xmlWrOp(file, "MeshPart", [("Material", "None")], indent + 1) tan = md.getTangents()for i, face inenumerate(md.faces): xmlWriteFace(file, face, tan[i], (0, 2, 1) , indent + 2)iflen(face.v) == 4: : # если грань – квад, то сохраняем второй треугольник xmlWriteFace(file, face, tan[i], (2, 0, 3) , indent + 2) xmlWrCl(file, "MeshPart", indent + 1) xmlWrCl(file, "Mesh", indent)# функция, записывающая в отдельный файл информацию о материалах всей сцены# это даст возможность после экспорта редактировать материалы объектов# здесь мы сохраняем цвет материала, ambient составляющую - силу света в тени,# данные о бликовой составляющей материала (цвет и силу блика), а так же# имена всех назначенных текстур объектуdef xmlWriteMaterals(filename): out = open(filename, "wt") xmlWrOp(out, "Materials")for mat in sceneMaterials:print"Export material '" + mat.name + "'" xmlWrOp(out, "Material", [("Name", mat.name)], 1) xmlWrOp(out, "Color", [("R", round(mat.R, 3)), ("G", round(mat.G, 3)), \("B", round(mat.B, 3)), ("A", round(mat.alpha, 3)), ("Ambient", round(mat.amb, 3))], 2, 1) xmlWrOp(out, "Specular", [("R", round(mat.specR, 3)), ("G", round(mat.specG, 3)), \("B", round(mat.specB, 3)), ("Power", round(mat.spec, 3))], 2, 1) xmlWrOp(out, "Textures", [], 1)for tex in mat.textures:if tex: image = tex.tex.imageif image: xmlWrOp(out, "Texture", [("Name", image.name)], 2, 1) xmlWrCl(out, "Textures", 1) xmlWrCl(out, "Material", 1) xmlWrCl(out, "Materials") out.close()# основная функция, сохраняющая общие данные всех поддерживаемых экспортером объектов,# так же для каждого типа объектов вызывает функции экспорта специфических данныхdef xmlWriteObgectData(file, obj, indent = 0):print"Export object '" + obj.name + "'"# первым делом создаем список атрибутов, и вносим в него имя и тип объекта objAttribs = [("Name", obj.name), ("Type", obj.type)]if obj.type == "Lamp": # добавляем в коллекцию атрибутов объекта параметры источника света objAttribs.append(("LampType", lampTypes[obj.getData().getType()]))if(lampTypes[obj.getData().getType()] == "Lamp"): objAttribs.append(("Dist", round(obj.getData().getDist(), 5))) objAttribs.append(("Energy", round(obj.getData().getEnergy(), 5)))elif obj.type == "Camera": # добавляем в коллекцию атрибутов объекта параметры камеры objAttribs.append(("Lens", round(convertLens(obj.getData().lens), 5))) objAttribs.append(("ClipStart", round(obj.getData().clipStart, 5))) objAttribs.append(("ClipEnd", round(obj.getData().clipEnd, 5)))# создаем запись узла дерева сцены со всеми атрибутами xmlWrOp(file, "Node", objAttribs, indent)# сохраняем трансформации объекта xmlWriteTransforms(file, obj, 2)if obj.type == "Mesh": # если объект – Mesh, экспортируем данные геометрии xmlWriteMeshData(file, obj, 2)elif obj.type == "Lamp": # для источников света сохраняем цвет lamp = obj.getData(); xmlWriteVector3D(file, "Color", ( lamp.G, lamp.B, lamp.R), 2);else: # для других типов объектов ничего не выполняемpass# сохраняем дополнительные параметры объекта xmlWrProperties(file, obj.getAllProperties(), 2)# сохраняем кривые анимации объекта xmlWriteIpo(file, obj, 2)# для всех потомков объекта, поддерживаемых экспортером, вызываем эту же функцию сохранения объектаfor child in Object.Get():if(child.typein supportedObjects)and(child.parent == obj): xmlWriteObgectData(file, child, indent + 1)# закрываем тэг записи узла дерева xmlWrCl(file, "Node", indent)# Главная функция экспортера, которая получает имя файла из диалога сохранения# в который будет выполнен экспортdef write(filename):print"Start export..." objects = []# находим все объекты, у которых нет родителя и поддерживаемые экспортеромfor obj in Object.Get():if(obj.typein supportedObjects)and(obj.parent == None): objects.append(obj)# открываем файл на запись в режиме текста out = open(filename, "wt")# записываем корневой элемент документа XML xmlWrOp(out, "Root")# записываем в файл все объекты, не имеющие родителейfor obj in objects: xmlWriteObgectData(out, obj, 1)# закрываем корневой элемент xmlWrCl(out, "Root")# закрываем файл out.close()# сохраняем в отдельный файл информацию о материалах# обратите внимание, здесь как раз используется срез, о котором я говорил ранее xmlWriteMaterals(filename[0:filename.rfind(".")] + ".materials.xml")print"Finished..."# первое что будет выполнено скриптом, это вызов диалога сохранения файла# если пользователь нажмет кнопку Export, то диалог сохранения вызовет функцию write# и будет выполнен экспортWindow.FileSelector(write, "Export", "level.xml")
После того, как скрипт готов, его необходимо сохранить. Существует несколько способов запустить написанный скрипт. Первый – из меню Text текстового редактора выбрать пункт Run Python Script. Второй – при активном окне текстового редактора (окно считается активным, если над ним расположен указатель мыши) нажать сочетание клавиш Alt+P. Эти два способа будут запускать скрипт, загруженный в текстовый редактор. Существует третий способ запуска скриптов экспорта – из главного меню File -> Export -> название скрипта, указанного в его заголовке. Но для того, чтобы сделать запуск скрипта возможным с использованием третьего способа, необходимо выполнить еще некоторое количество действий. Первое что необходимо сделать – создать папку, в которой будут находится Ваши созданные скрипты. Для примера создадим следующую папку на диске C: - «C:\BlenderScripts\». Далее необходимо указать Blender’у, что в этой папке находятся Ваши скрипты (собственно говоря там могут находится и не только Ваши скрипты, а еще и скачанные скрипты, расширяющие возможности Blender). Сделать это можно следующим образом:
Перетащить мышкой главное меню Blender’а вниз. Это откроет настройки программы.
Переключиться в режим редактирования путей.
Указать в поле Python Scripts папку, которую мы создали ранее.
Минимизировать окно настроек, перетащив его заголовок обратно вверх.
Выбрать в меню File пункт Save Default Settings (или нажать сочетание Ctrl+U) для сохранения настроек по умолчанию. Сохранение настроек по умолчанию необходимо для сохранения пути к папке со скриптами.
Результатом выполненных действий в списке подпунктов меню File -> Export появится пункт XnaCev.Ru Exporter Example, выбрав который, будет вызван диалог выбора файла, т.е запущен наш с Вами скрипт экспорта..
На следующем скриншоте отображен результат проделанных действий:
Итак все готово для экспорта, но есть одно но. Наш скрипт не может генерировать текстурные координаты для объектов. Это необходимо сделать до экспорта объектов, иначе при загрузке сцены в приложение XNA вы ничего не увидите т.к. наш скрипт экспортирует узел дерева сцены, но без информации о геометрии. Такое поведение можно увидеть при экспорте сцены по умолчанию в консоли Blender’а «Mesh ‘Cube’ has no UV, skipped.»:
Если добавить кубу текстурные координаты, то мы увидим следующий результат:
Добавить текстурные координаты объекту можно в режиме редактирования сетки (чтобы перейти в режим редактирования сетки необходимо при выбранном объекте нажать клавишу Tab либо выбрать пункт Edit Mode из выпадающего списка, расположенного справа от пункта меню Object окна редактирования 3D модели – 3D View) с помощью горячей клавиши U.
Как видно, кубу не назначены ни одной текстуры, но материал по умолчанию экспортировался нормально.
В данной статье будет взята за основу несколько упрощенная сцена перекрестка, сделанная моим другом Akima (на самом деле этот перекресток он сделал по моей просьбе для данной статьи). Я добавил туда пример простой анимации, чтобы продемонстрировать анимацию на основе кривых Безье. Результат загрузки данной сцены в XNA/XEN проект Вы увидите в следующей части статьи.
Если Вы посмотрите на правый верхний угол Blender, то увидите что в тестовой сцене порядка 16к вершин и полигонов. Это позволит нам выполнить тест формата XML на время загрузки уровня, т.к. экспортированная сцена занимает порядка 13 мегабайт. Для такой маленькой сцены это очень много, но зато позволит нам наглядно изучить состав файла экспортированного уровня изнутри. Кроме того перевод такого количества значений с плавающей точкой из строк значительно увеличит время загрузки сцены, так что не волнуйтесь, если после запуска не увидите окно проекта сразу.
Дополнительные материалы.
Добавление дополнительных свойств объектам сцены.
Как я уже говорил, в Blender’е есть свой игровой движок. В следствие этого, в нем присутствуют средства, которые пригодятся нам для своей игры или другого интерактивного приложения, написанного на XNA. Основная возможность, которая меня заинтересовала в первую очередь – это возможность добавления различных пользовательских параметров объектам сцены. Чтобы добавить дополнительные параметры объекту необходимо:
выбрать этот объект;
на кнопочной панели (данная панель по умолчанию находится внизу всех режимов редактирования) переключиться в режим Logic (Логика) [1];
нажать на кнопку Add Property (Добавить Свойство) [2].
на появившейся строке ввести имя свойства [1] и его значение [2].
Кроме имени и значения необходимо выбрать тип данных свойства. Выпадающий список слева от имени каждого свойства позволит выбрать одно из следующих значений: Timer, String, Float, Int и Bool. Первое нам не нужно, зато остальные вполне могут использоваться в качестве типов параметров объектов.
Я не занимался исследованиями максимально возможного количества свойств, которое можно добавить каждому объекту, но думаю, этого количества хватит для любых ваших проектов.
Анимация в Blender.
Так как частью статьи является экспорт анимированных объектов, я хочу немного рассказать как начать создавать анимацию. Прежде всего необходимо перейти в режим создания анимации. Как это сделать отображено на следующем скриншоте:
По умолчанию, режим редактирования анимации выглядит, как показано на следующем скриншоте:
Данный режим состоит из пяти окон, которые могут понадобиться аниматору для создания анимации:
Outliner – навигатор по дереву сцены.
3D View – окно 3D редактора.
IPO Curve Editor – редактор кривых.
Timeline – шкала времени.
Buttons Window – окно с кнопками.
Для того, чтобы Blender автоматически записывал ключевые кадры анимации необходимо включить режим записи и выбрать режим добавления/замещения (Add/Replace) кадров. В результате при перетаскивании, вращении или масштабировании объектов будут созданы ключевые кадры для текущего положения на временной шкале. На основе ключевых кадров создаются кривые Безье. Эти кривые мы и экспортировали.
Как редактировать кривые анимации будет Вашим домашним заданием. На этом первая часть статьи закончена, впереди вторая не менее увлекательная.