Навигация
· XNA FAQ
· С чего начать
· Конкурсы
· Обратная связь
· XNA Блоги
Сейчас на сайте
· Гостей: 1

· Пользователей: 0

· Всего пользователей: 3,684
· Новый пользователь: headron
Последние фото
Эх, чуть не проспал закрытие.
Эх, чуть не проспал ...
Альбом: XNA Engine

GB
GB
Альбом: XNA Engine

South Park Coon & Friends
South Park Coon & Fr...
Альбом: XNA Games

Блоги
yavshoke
» XboxOne - интерес...
dampirik
» Push уведомления ...
dampirik
» Реклама,статистик...
Chort
» XNA и StartCoroutine
Chort
» Curve Class
dampirik
» Реклама, статисти...
dampirik
» Увеличение скорос...
dampirik
» Реклама, статисти...
general
» Распаковка DxtCom...
general
» Как работать с XN...
Поддержка
microsoft.com
1gb.ru - Дом для вашего сайта
Статистика посещений:

Blender + Xna + Xen = ??? (часть 1)
Введение

Данная статья является моими собственными исследования в использовании 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 с открытым исходным кодом. Скачать его можно по адресу: http://xen.codeplex.com


Не смотря на то, что статья написана для Xen, все, что описано в ней можно реализовать и на XNA, но тогда Вам придется самим разработать много дополнительного кода такого как, например, по вывод на экран с сортировкой по глубине.

К статье прилагаются все исходные коды проекта, скрипт экспорта из Blender, тестовая сцена для экспорта. Все это можно скачать одним архивом по следующей ссылке:

Материалы и Media, использованные в статье:

Документация по Blender Python API: http://www.blender.org/documentation/249PythonDoc/
Описание кривых Безье: http://ru.wikipedia.org/wiki/Кривая_Безье
В качестве документации по Xen были использованы примеры, входящие в его состав (в состав Xen входит 25 примеров с подробным описанием на английском языке).
Для бонуса к статье (реализация SkyBox на Xen) использовалась медиа из примера реализации воды, также за основу была взята реализация скайбокса из этого примера: http://www.codeplex.com/wikipage?ProjectName=XNACommunity&title=Water
Модель перекрестка была создана моим другом Akima.

Данная статься, можно сказать, является не только описанием загрузки сцены, в статье так же рассматривается создание графа сцены и др. составляющих игрового движка. Альтернативную реализацию графа сцены и др. составляющих движка можно посмотреть в альтернативной реализации Дмитрия Тимофеева (General) – движка gEngine


Исходный код к статье можно скачать по адресу: (http://xnadev.ru/downloads.php?page_id=50)

Итак, экспорт сцены из Blender.


Прежде всего, немного теории.


Blender – бесплатный пакет трехмерного моделирования. Возможностей последней стабильной версии хватает для создания таких мультфильмов как Elephants Dream, Big Buck Bunny и др. Как и все платные пакеты 3D моделирования, такие как 3DS MAX или Maya, Blender имеет возможность расширения своих функций, путем создания скриптов на языке Python.
Чтобы эффективно писать скрипты для Blender, необходимо знать, откуда взять информацию о Python – API функциях , которые можно применять в его скриптах.
Самая свежая документация по Python API Blender’а находится по адресу http://www.blender.org/documentation/249PythonDoc/

Каждый объект сцены в 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: 248
Group: 'Export'
Tooltip: 'Exports data from Blender to XNA'
"""

 

Далее импорт пространств имен и объектов пространств имен в текущее пространство имен (пространство имен скрипта)

Импорт
1
2
3
4
5
import os
import math
import bpy
import Blender
from Blender import *

 

Прежде всего, оговорюсь о том, как должна быть построена программа на питоне. Т.к. питон – интерпретируемый язык, то необходимо писать все функции перед из использованием. Этими функциями в нашем случае будут функции записи XML-тэгов в текстовый документ. Все функции, записывающие информацию в файл, будут получать первым параметром ссылку на объект файла, который будет содержать методы для записи в файл.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# функция записи в файл отступа от края документа
def wrIndent(file, indent): 
  if indent:
    for i in xrange(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’а к системе координат XNA
def 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, для использования при создании камеры в XNA
def convertLens(lens):
  return 2 * math.atan(16 / lens)
 
 
# коллекция типов источников света
lampTypes = ('Lamp', 'Sun', 'Spot', 'Hemi', 'Area', 'Photon' )
 
# коллекция типов продолжения кривых анимации на концах промежутков анимации
ipoExtendTypes = ("CONST", "EXTRAP", "CYCLIC", "CYCLIC_EXTRAP")
 
# коллекция поддерживаемых экспортером объектов 
supportedObjects = ('Mesh', 'Empty', 'Lamp', 'Camera')
 
# переменная списка, в которую будут записаны все материалы экспортируемых объектов
sceneMaterials = []
 
# коллекции необходимее для определения «правильных» имен кривых при экспорте
# основная задача – приведение анимации в координатную систему XNA
ipoFrom = ('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 in enumerate(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.vec
    if (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:
      pass
  elif obj.data: # контейнер, специфичный для каждого типа объектов
    try:
      ipo = obj.data.ipo
      if 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) # данные о геометрии содержатся в контейнере объекта
 
  # первое что мы должны проверить – это наличие текстурных координат
  if not 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.materials
  if materials:
    for matIndex in xrange(len(materials)):
      if not materials[matIndex] in sceneMaterials:
        sceneMaterials.append(materials[matIndex])
      xmlWrOp(file, "MeshPart", [("Material", materials[matIndex].name)], indent + 1)
      tan = md.getTangents()
      for i, face in enumerate(md.faces):
        if face.mat == matIndex:
          xmlWriteFace(file, face, tan[i], (0, 2, 1) , indent + 2)
          if len(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 in enumerate(md.faces):
      xmlWriteFace(file, face, tan[i], (0, 2, 1) , indent + 2)
      if len(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.image
        if 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.type in 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.type in 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). Сделать это можно следующим образом:
  1. Перетащить мышкой главное меню Blender’а вниз. Это откроет настройки программы.
  2. Переключиться в режим редактирования путей.
  3. Указать в поле Python Scripts папку, которую мы создали ранее.
  4. Минимизировать окно настроек, перетащив его заголовок обратно вверх.
  5. Выбрать в меню File пункт Save Default Settings (или нажать сочетание Ctrl+U) для сохранения настроек по умолчанию. Сохранение настроек по умолчанию необходимо для сохранения пути к папке со скриптами.
  6. Результатом выполненных действий в списке подпунктов меню File -> Export появится пункт XnaCev.Ru Exporter Example, выбрав который, будет вызван диалог выбора файла, т.е запущен наш с Вами скрипт экспорта..


На следующем скриншоте отображен результат проделанных действий:



Итак все готово для экспорта, но есть одно но. Наш скрипт не может генерировать текстурные координаты для объектов. Это необходимо сделать до экспорта объектов, иначе при загрузке сцены в приложение XNA вы ничего не увидите т.к. наш скрипт экспортирует узел дерева сцены, но без информации о геометрии. Такое поведение можно увидеть при экспорте сцены по умолчанию в консоли Blender’а «Mesh ‘Cube’ has no UV, skipped.»:



Если добавить кубу текстурные координаты, то мы увидим следующий результат:



Добавить текстурные координаты объекту можно в режиме редактирования сетки (чтобы перейти в режим редактирования сетки необходимо при выбранном объекте нажать клавишу Tab либо выбрать пункт Edit Mode из выпадающего списка, расположенного справа от пункта меню Object окна редактирования 3D модели – 3D View) с помощью горячей клавиши  U.

Результат  экспорта такой сцены будет следующим:

level.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<Root>
  <Node Name="Camera" Type="Camera" Lens="0.85756" ClipStart="0.1" ClipEnd="100.0">
    <Transforms>
      <Pos X="-6.50764" Y="5.34367" Z="7.48113" />
      <Rot X="0.61977" Y="46.69194" Z="63.5593" />
      <Scl X="1.0" Y="1.0" Z="1.0" />
    </Transforms>
    <Properties>
    </Properties>
    <Ipo>
    </Ipo>
  </Node>
  <Node Name="Cube" Type="Mesh">
    <Transforms>
      <Pos X="0.0" Y="0.0" Z="0.0" />
      <Rot X="0.0" Y="0.0" Z="0.0" />
      <Scl X="1.0" Y="1.0" Z="1.0" />
    </Transforms>
    <Mesh>
      <Vertices Count="8">
        <Vert Index="0" X="1.0" Y="-1.0" Z="1.0" />
        <Vert Index="1" X="-1.0" Y="-1.0" Z="1.0" />
        <Vert Index="2" X="-1.0" Y="-1.0" Z="-1.0" />
        <Vert Index="3" X="1.0" Y="-1.0" Z="-1.0" />
        <Vert Index="4" X="1.0" Y="1.0" Z="1.0" />
        <Vert Index="5" X="-1.0" Y="1.0" Z="1.0" />
        <Vert Index="6" X="-1.0" Y="1.0" Z="-1.0" />
        <Vert Index="7" X="1.0" Y="1.0" Z="-1.0" />
      </Vertices>
      <MeshPart Material="Material">
        <Face>
          <V Index="0" NX="0.0" NY="-1.0" NZ="0.0" TX="0.70711" TY="-0.70711" TZ="0.0" UVX="0.0" UVY="0.0" />
          <V Index="2" NX="0.0" NY="-1.0" NZ="0.0" TX="1.0" TY="0.0" TZ="0.0" UVX="1.0" UVY="1.0" />
          <V Index="1" NX="0.0" NY="-1.0" NZ="0.0" TX="1.0" TY="0.0" TZ="0.0" UVX="1.0" UVY="0.0" />
        </Face>
        <Face>
          <V Index="2" NX="0.0" NY="-1.0" NZ="0.0" TX="1.0" TY="0.0" TZ="0.0" UVX="1.0" UVY="1.0" />
          <V Index="0" NX="0.0" NY="-1.0" NZ="0.0" TX="0.70711" TY="-0.70711" TZ="0.0" UVX="0.0" UVY="0.0" />
          <V Index="3" NX="0.0" NY="-1.0" NZ="0.0" TX="0.70711" TY="-0.70711" TZ="0.0" UVX="0.0" UVY="1.0" />
        </Face>
        <Face>
          <V Index="4" NX="0.0" NY="1.0" NZ="0.0" TX="-0.0" TY="0.70711" TZ="0.70711" UVX="0.0" UVY="0.0" />
          <V Index="6" NX="0.0" NY="1.0" NZ="0.0" TX="-0.0" TY="-0.70711" TZ="0.70711" UVX="1.0" UVY="1.0" />
          <V Index="7" NX="0.0" NY="1.0" NZ="0.0" TX="-0.0" TY="0.0" TZ="1.0" UVX="1.0" UVY="0.0" />
        </Face>
        <Face>
          <V Index="6" NX="0.0" NY="1.0" NZ="0.0" TX="-0.0" TY="-0.70711" TZ="0.70711" UVX="1.0" UVY="1.0" />
          <V Index="4" NX="0.0" NY="1.0" NZ="0.0" TX="-0.0" TY="0.70711" TZ="0.70711" UVX="0.0" UVY="0.0" />
          <V Index="5" NX="0.0" NY="1.0" NZ="0.0" TX="-0.0" TY="0.0" TZ="1.0" UVX="0.0" UVY="1.0" />
        </Face>
        <Face>
          <V Index="0" NX="-0.0" NY="0.0" NZ="1.0" TX="0.70711" TY="-0.70711" TZ="0.0" UVX="0.0" UVY="0.0" />
          <V Index="5" NX="-0.0" NY="0.0" NZ="1.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="1.0" UVY="1.0" />
          <V Index="4" NX="-0.0" NY="0.0" NZ="1.0" TX="0.0" TY="-1.0" TZ="-0.0" UVX="1.0" UVY="0.0" />
        </Face>
        <Face>
          <V Index="5" NX="-0.0" NY="0.0" NZ="1.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="1.0" UVY="1.0" />
          <V Index="0" NX="-0.0" NY="0.0" NZ="1.0" TX="0.70711" TY="-0.70711" TZ="0.0" UVX="0.0" UVY="0.0" />
          <V Index="1" NX="-0.0" NY="0.0" NZ="1.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="0.0" UVY="1.0" />
        </Face>
        <Face>
          <V Index="1" NX="-1.0" NY="-0.0" NZ="-0.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="0.0" UVY="0.0" />
          <V Index="6" NX="-1.0" NY="-0.0" NZ="-0.0" TX="-0.0" TY="-0.70711" TZ="0.70711" UVX="1.0" UVY="1.0" />
          <V Index="5" NX="-1.0" NY="-0.0" NZ="-0.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="1.0" UVY="0.0" />
        </Face>
        <Face>
          <V Index="6" NX="-1.0" NY="-0.0" NZ="-0.0" TX="-0.0" TY="-0.70711" TZ="0.70711" UVX="1.0" UVY="1.0" />
          <V Index="1" NX="-1.0" NY="-0.0" NZ="-0.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="0.0" UVY="0.0" />
          <V Index="2" NX="-1.0" NY="-0.0" NZ="-0.0" TX="-0.0" TY="-1.0" TZ="0.0" UVX="0.0" UVY="1.0" />
        </Face>
        <Face>
          <V Index="2" NX="0.0" NY="-0.0" NZ="-1.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="0.0" UVY="0.0" />
          <V Index="7" NX="0.0" NY="-0.0" NZ="-1.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="1.0" UVY="1.0" />
          <V Index="6" NX="0.0" NY="-0.0" NZ="-1.0" TX="-0.0" TY="-1.0" TZ="0.0" UVX="1.0" UVY="0.0" />
        </Face>
        <Face>
          <V Index="7" NX="0.0" NY="-0.0" NZ="-1.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="1.0" UVY="1.0" />
          <V Index="2" NX="0.0" NY="-0.0" NZ="-1.0" TX="0.0" TY="-1.0" TZ="0.0" UVX="0.0" UVY="0.0" />
          <V Index="3" NX="0.0" NY="-0.0" NZ="-1.0" TX="0.70711" TY="-0.70711" TZ="0.0" UVX="0.0" UVY="1.0" />
        </Face>
        <Face>
          <V Index="4" NX="1.0" NY="0.0" NZ="0.0" TX="-0.0" TY="0.70711" TZ="0.70711" UVX="0.0" UVY="0.0" />
          <V Index="3" NX="1.0" NY="0.0" NZ="0.0" TX="-0.0" TY="1.0" TZ="0.0" UVX="1.0" UVY="1.0" />
          <V Index="0" NX="1.0" NY="0.0" NZ="0.0" TX="-0.0" TY="1.0" TZ="0.0" UVX="1.0" UVY="0.0" />
        </Face>
        <Face>
          <V Index="3" NX="1.0" NY="0.0" NZ="0.0" TX="-0.0" TY="1.0" TZ="0.0" UVX="1.0" UVY="1.0" />
          <V Index="4" NX="1.0" NY="0.0" NZ="0.0" TX="-0.0" TY="0.70711" TZ="0.70711" UVX="0.0" UVY="0.0" />
          <V Index="7" NX="1.0" NY="0.0" NZ="0.0" TX="-0.0" TY="1.0" TZ="-0.0" UVX="0.0" UVY="1.0" />
        </Face>
      </MeshPart>
    </Mesh>
    <Properties>
    </Properties>
    <Ipo>
    </Ipo>
  </Node>
  <Node Name="Lamp" Type="Lamp" LampType="Lamp" Dist="29.99998" Energy="1.0">
    <Transforms>
      <Pos X="1.00545" Y="5.90386" Z="4.07625" />
      <Rot X="3.16371" Y="106.93632" Z="37.26105" />
      <Scl X="1.0" Y="1.0" Z="1.0" />
    </Transforms>
    <Color X="1.0" Y="1.0" Z="1.0" />
    <Properties>
    </Properties>
    <Ipo>
    </Ipo>
  </Node>
</Root>

 

Level.Materials.xml
1
2
3
4
5
6
7
8
<Materials>
  <Material Name="Material">
    <Color R="0.8" G="0.8" B="0.8" A="1.0" Ambient="0.5" />
    <Specular R="1.0" G="1.0" B="1.0" Power="0.5" />
  <Textures>
  </Textures>
  </Material>
</Materials>

 


Как видно, кубу не назначены ни одной текстуры, но материал по умолчанию экспортировался нормально.

В данной статье будет взята за основу несколько упрощенная сцена перекрестка, сделанная моим другом Akima (на самом деле этот перекресток он сделал по моей просьбе для данной статьи). Я добавил туда пример простой анимации, чтобы продемонстрировать анимацию на основе кривых Безье. Результат загрузки данной сцены в XNA/XEN проект Вы увидите в следующей части статьи.



Если Вы посмотрите на правый верхний угол Blender, то увидите что в тестовой сцене порядка 16к вершин и полигонов. Это позволит нам выполнить тест формата XML на время загрузки уровня, т.к. экспортированная сцена занимает порядка 13 мегабайт. Для такой маленькой сцены это очень много, но зато позволит нам наглядно изучить состав файла экспортированного уровня изнутри. Кроме того перевод такого количества значений с плавающей точкой из строк значительно увеличит время загрузки сцены, так что не волнуйтесь, если после запуска не увидите окно проекта сразу.

Дополнительные материалы.

Добавление дополнительных свойств объектам сцены.

Как я уже говорил, в Blender’е есть свой игровой движок. В следствие этого, в нем присутствуют средства, которые пригодятся нам для своей игры или другого интерактивного приложения, написанного на XNA. Основная возможность, которая меня заинтересовала в первую очередь – это возможность добавления различных пользовательских параметров объектам сцены.
Чтобы добавить дополнительные параметры объекту необходимо:
  • выбрать этот объект;
  • на кнопочной панели (данная панель по умолчанию находится внизу всех режимов редактирования) переключиться в режим Logic (Логика) [1];
  • нажать на кнопку Add Property (Добавить Свойство) [2].


  • на появившейся строке ввести имя свойства [1] и его значение [2].



Кроме имени и значения необходимо выбрать тип данных свойства. Выпадающий список слева от имени каждого свойства позволит выбрать одно из следующих значений: Timer, String, Float, Int и Bool. Первое нам не нужно, зато остальные вполне могут использоваться в качестве типов параметров объектов.

Я не занимался исследованиями максимально возможного количества свойств, которое можно добавить каждому объекту, но думаю, этого количества хватит для любых ваших проектов.


Анимация в Blender.

Так как частью статьи является экспорт анимированных объектов, я хочу немного рассказать как начать создавать анимацию.
Прежде всего необходимо перейти в режим создания анимации. Как это сделать отображено на следующем скриншоте:



По умолчанию, режим редактирования анимации выглядит, как показано на следующем скриншоте:



Данный режим состоит из пяти окон, которые могут понадобиться аниматору для создания анимации:
  1. Outliner – навигатор по дереву сцены.
  2. 3D View – окно 3D редактора.
  3. IPO Curve Editor – редактор кривых.
  4. Timeline – шкала времени.
  5. Buttons Window – окно с кнопками.

Для того, чтобы Blender автоматически записывал ключевые кадры анимации необходимо включить режим записи и выбрать режим добавления/замещения (Add/Replace) кадров. В результате при перетаскивании, вращении или масштабировании объектов будут созданы ключевые кадры для текущего положения на временной шкале. На основе ключевых кадров создаются кривые Безье. Эти кривые мы и экспортировали.



Как редактировать кривые анимации будет Вашим домашним заданием. На этом первая часть статьи закончена, впереди вторая не менее увлекательная.
Комментарии
#1 | Waldemar 26.10.2009 17:19:12
Очень круто! Объём работы, видимо, просто колоссальный!
Я, пожалуй, создам отдельную тему, чтобы позадавать свои вопросы. Рассчитываю на ответы от автора Smile
#2 | pax 26.10.2009 18:27:35
Извиняюсь за большой размер... короче не получилось. Рад что в движок встроена подсветка синтаксиса питона Smile
Вторая часть не меньше Wink
#3 | dyvniy 09.11.2009 15:10:21
Здорово! А я-то мучяюсь с максом и плагинами для экспорта в *.Х . Буду юзать блендер и учить питон.
Добавить комментарий
Пожалуйста, залогиньтесь для добавления комментария.
Рейтинги
Рейтинг доступен только для пользователей.

Пожалуйста, залогиньтесь или зарегистрируйтесь для голосования.

Отлично! Отлично! 86% [6 Голоса]
Очень хорошо Очень хорошо 14% [1 Голос]
Хорошо Хорошо 0% [Нет голосов]
Удовлетворительно Удовлетворительно 0% [Нет голосов]
Плохо Плохо 0% [Нет голосов]
Авторизация
Логин

Пароль



Вы не зарегистрированы?
Нажмите здесь для регистрации.

Забыли пароль?
Запросите новый здесь.
Мини-чат
Вы должны авторизироваться, чтобы добавить сообщение.

27.08.2014
Я умею немного на asp.net + html и css

22.08.2014
на ASP mvc 3 есть пару проектов. Могу помочь, если нужно. Обидно, если закроется Frown

21.08.2014
я тоже ноль

21.08.2014
Я в вебе только с php занимался да и то на уровне чтоб работало.

21.08.2014
Я в вебе полный ноль…

21.08.2014
Переводить его надо, хоть на ту же азуру. И двиг менять на что-то современное. Если есть веб-разрабы - можем скооперироваться. Один делать не буду.

21.08.2014
не знаю всех нюансов по оплате и все хорошее когда нибудь заканчивается

21.08.2014
А что случилось?

21.08.2014
похоже сайт будет работать до 28го числа

09.08.2014
Апи пока не видел. Но есть приложение в магазине Live Lock Screen BETA, так что думаю скоро будет

08.08.2014
Я про API для Update1. На нем работает это

08.08.2014
А что именно нужно? Чтото и сейчас открыто http://msdn.micro.
...105).aspx

06.08.2014
Кто-нибудь слышал об открытии доступа к Lock Screen Api?

31.07.2014
VPDExpress на базе MVS 2012, ни в какую не ловит исключения. Даже если их сам создаешь. И всех так?

25.07.2014
С днем системного администратора причастных к этой профессии! По случаю - тортик от жены

RSS каналы сайта
XNA - Новости
XNA - Статьи
XNA - Форум
XNA - Галерея
XNA - Файлы
Время загрузки: 0,05 секунд 8,709,519 уникальных посетителей