Программируем трехмерную графику с Irrlicht . Часть 11

August 11, 2007

Эффективное программирование 3d-приложений с помощью Irrlicht и jython. Часть 11



В прошлый раз мы научились проектировать интерфейс приложения, используя стандартные компонентов GUI: кнопки, списки, диалоговые окна. Сегодня мы завершаем эту тему. Так нам осталось рассмотреть методику обработки событий от этих компонентов. А также мы попробуем загрузить в среду irrlicht уровень от quake2/3.

Напоминаю, что очень-очень важно при проектировании интерфейса дать всем используемым компонентам уникальные идентификаторы. В противном случае, вы не сможете определить, какой именно элемент был активирован пользователем. Для знакомых с идеями delphi/cbuilder/.net методика обработки событий в irrlicht может показаться очень примитивной. При создании конкретного элемента управления мы не можем привязать к нему обработчик события, а должны внутри уже знакомой нам функции “def OnEvent (self, e)” определить к какому классу относится произошедшее событие. Если оно относится к семейству GUI Events, то уже следует определить, что за элемент и какое специфическое для него событие случилось. Для тех, кто когда-то писал программы под классическое winapi (без позднее появившихся MFC), у вас есть возможность вернуться в босоногое детство. Отчасти подобное ретроградство можно объяснить не слишком богатым набором компонентов, отчасти тем, что поддержка GUI в irrlicht да и почти во всех остальных 3d движках никогда не было приоритетным направлением. Большей частью такая методика обработки событий не представляет никаких сложностей, разве что больший объем кода, но в определенных ситуациях возникает настоящая угроза идеям ООП: я говорю об обработке событий от всплывающих окон. В примере ниже я показал это с помощью MessageBox - окна вопроса с двумя вариантами выбора. Дело в том, что события от диалогового окна (даже если он модальный) все равно приходят и анализируются единой для всего приложения функцией обработки сообщений.

Если вы будете активно использовать irrlicht gui, то я рекомендую потратить предварительно время на создание собственной надстройки над моделью событий. Наиболее просто будет реализовать нечто подобное картам событий в MFC. Если вы знакомы с паттернами программирования, то сможете подобрать сами наиболее подходящий паттерн поведения.

В примере ниже создается список, текстовое поле и кнопки. При нажатии на кнопку “Append” содержимое текстового поля добавляется в конец списка. При этом выполняется проверка, чтобы текстовое поле было не пустым (в случае необходимости выводится окно сообщения ошибки). При нажатии на кнопку “Choose” появляется окно выбора из двух вариантов (YES|NO). Какая бы кнопка не была нажата, в любом случае сообщение добавляется в список “lst_log”. Кнопка “File Dialog” приводит к появлению диалога выбора файла. И, наконец, кнопка “Color Dialog” приводит к появлению диалога выбора цвета (поддержка данной возможности появилась в irrlicht начиная с версии 1.2).

Для отображения окна выбора файла используется функция:
 addFileOpenDialog(title, modal, parent, id)
Параметрами этой функции служат (по порядку) – текст заголовка сообщения, признак того будет ли окно выбора файла модальным или нет, родительское окно и, привычный уже нам, уникальный идентификатор окна диалога.

Для того чтобы показать окно выбора цвета используется метод addColorSelectDialog, параметры которого идентичны тем, которые принимает и метод addFileOpenDialog.

Общим для всех диалоговых окон является то, что после их отображения на экране irrlicht начинает генерировать слишком большое количество сообщений, например простое перемещение мыши над окном диалога уже приводит к генерации событий-сообщений. Нам нужно грамотно отбрасывать информационный мусор и реагировать только на события закрытия диалогового окна, с помощью любой из управляющих кнопок.

В примере я также создаю меню окна irrlicht. Для добавления меню достаточно использовать метод addMenu() без параметров. Возможно строить многоуровневые меню. Это достигается за счет того, что при вызове метода добавления пункта addItem можно указать специальный флаг “hasSubMenu”. Затем следует получить ссылку на добавленный пункт c помощью getSubMenu, и от его имени делать вызовы функции добавления новых подпунктов. Если вы хотите отделить пункты меню с помощью линии-разделителя, то используйте метод addSeparator.
  1. import java
  2.  
  3. import net.sf.jirr
  4. from net.sf.jirr import dimension2di
  5. from net.sf.jirr import position2di
  6. from net.sf.jirr import recti
  7. from net.sf.jirr import SColor
  8. from net.sf.jirr import IEventReceiver
  9.  
  10. # стандартно класс обработчика событий наследуется от IEventReceiver 
  11. class EvtHandler (IEventReceiver):
  12.  # в этих двух переменных будут находиться ссылки на 
  13. открытые диалоговые окна - выбора файлов и выбора цвета
  14.  cdialog = None
  15.  fdialog = None
  16.  
  17.  # конструктор класса обработки событий
  18.  def __init__ (self):
  19.  IEventReceiver.__init__ (self)
  20.  
  21.  def OnEvent (self, e):
  22.  if e.getEventType() == net.sf.jirr.EEVENT_TYPE.EET_GUI_EVENT:
  23.  # определяем что событие относится к семейству событий GUI
  24.  if GUI_MESSAGEBOX_DIALOG == e.getGUIEventCaller ().getID():
  25.  # теперь проверяем если инициатор события MsgBox – 
  26. окно сообщения с двумя кнопками YES и NO, то проверяем уточняющий тип сообщения 
  27. - какая именно кнопка была нажата
  28.  if net.sf.jirr.EGUI_EVENT_TYPE.EGET_MESSAGEBOX_YES == e.getGUIEventType ():
  29.   lst_log.addItem ("MsgBox YES")
  30.  if net.sf.jirr.EGUI_EVENT_TYPE.EGET_MESSAGEBOX_NO == e.getGUIEventType ():
  31.   lst_log.addItem ("MsgBox NO")
  32.  
  33.  if GUI_FILEOPEN_DIALOG == e.getGUIEventCaller ().getID():
  34.  # событие закрытие диалога выбора файла
  35. if net.sf.jirr.EGUI_EVENT_TYPE.EGET_FILE_SELECTED == e.getGUIEventType ():
  36.   lst_log.addItem ("File: " + self.fdialog.getFilename () )
  37.  
  38.  if e.getGUIEventType () == net.sf.jirr.EGUI_EVENT_TYPE.EGET_BUTTON_CLICKED:
  39.  # если подтип события - это нажатие кнопки то я проверяю какая из
  40.  четырех кнопок была активирована и выполняю соответствующие действия
  41.  if e.getGUIEventCaller ().getID() == GUI_BUTTON_ADD:
  42.   if txt_fio.getText() == '':
  43.   # сообщение об ошибке если поле пустое
  44.   guienv.addMessageBox ("Error", "Text Field Is Empty, cannot add")
  45.   return False
  46.   lst_log.addItem (txt_fio.getText())
  47.  if e.getGUIEventCaller ().getID() == GUI_BUTTON_CHOOSE:
  48.   # при нажатии на кнопку Choose следует показать диалоговое 
  49. окно выбора из двух вариантов,
  50.   # Первый параметр - это заголовок диалогового окна, второй 
  51. - текст сообщения, третий параметр - отвечает за то будет ли 
  52. наше окно сообщения модальным или нет, последний параметр - 
  53. это набор отображаемых кнопок окна диалога, 
  54. обратите внимание на то, как именно я выполняю сборку набора 
  55. - через оператор битового или "|" 
  56.   guienv.addMessageBox ("Question", "What Do Your Want ?", True, 
  57. net.sf.jirr.EMESSAGE_BOX_FLAG.EMBF_YES.swigValue() | 
  58. net.sf.jirr.EMESSAGE_BOX_FLAG.EMBF_NO.swigValue() , None, GUI_MESSAGEBOX_DIALOG)
  59.  
  60.   lst_log.addItem ("MsgBox Showed")
  61.  if e.getGUIEventCaller ().getID() == GUI_BTN_DIALOG_FILE:
  62.   self.fdialog = guienv.addFileOpenDialog ("Select File", True, None, GUI_FILEOPEN_DIALOG)
  63.  if e.getGUIEventCaller ().getID() == GUI_BTN_DIALOG_COLOR:
  64.   self.cdialog = guienv.addColorSelectDialog ("Select Color", True, None, GUI_COLOR_DIALOG)
  65.  return False
  66.  
  67. java.lang.System.loadLibrary ('irrlicht_wrap')
  68. device = net.sf.jirr.Jirr.createDevice(net.sf.jirr.E_DRIVER_TYPE.EDT_DIRECT3D9, 
  69. dimension2di(800, 600), 32)
  70. driver = device.getVideoDriver()
  71. guienv = device.getGUIEnvironment()
  72.  
  73. # объявляем список идентификаторов элементов управления
  74. GUI_BUTTON_ADD = 103
  75. GUI_BUTTON_CHOOSE = 104
  76. GUI_MESSAGEBOX_DIALOG = 105
  77. GUI_FILEOPEN_DIALOG = 106
  78. GUI_COLOR_DIALOG = 107
  79. GUI_BUTTON_SAVE = 108
  80. GUI_LIST_SOURCE = 109
  81. GUI_BTN_DIALOG_FILE = 110
  82. GUI_BTN_DIALOG_COLOR = 111
  83. GUI_TEXT_SOURCE = 112
  84.  
  85. # эти идентификаторы будут служить для управления пунктами меню
  86. GUI_MNN_APPLE = 113
  87. GUI_MNN_ORANGE = 114
  88. GUI_MNN_ORANGE_1 = 115
  89. GUI_MNN_ORANGE_2 = 116
  90.  
  91. # создаем кнопки
  92. btn_add = guienv.addButton(recti(120,70,190,90), None, GUI_BUTTON_ADD, "Append")
  93. btn_remove = guienv.addButton(recti(120,100,190,120), None, GUI_BUTTON_CHOOSE, "Choose")
  94. btn_move_left = guienv.addButton(recti(120,200,190,220), None, GUI_BTN_DIALOG_FILE, "File Dialog")
  95. btn_move_left = guienv.addButton(recti(120,230,190,250), None, GUI_BTN_DIALOG_COLOR, "Color Dialog")
  96.  
  97. # создаем список
  98. lst_log = guienv.addListBox(recti(10,100,110,400), None, GUI_LIST_SOURCE)
  99. # создаем текстовое поле
  100. txt_fio = guienv.addEditBox("Potatoes", recti(10,70,110,90), True, None, GUI_TEXT_SOURCE)
  101.  
  102. # создаем меню
  103. menus = guienv.addMenu ()
  104. # добавляем к меню первый пункт - это обычный пункт без дочерних подпунктов
  105. menus.addItem ("Apple", GUI_MNN_APPLE)
  106. # добавим разделитель между пунктами меню
  107. menus.addSeparator()
  108. # а теперь при вызове addItem мы укажем больше параметров, 
  109. в том числе признак того, что этот пункт меню будет содержать дочерние подпункты
  110. # предпоследний параметр отвечает за то будет ли данный пункт 
  111. меню доступным или нет (т.е. заблокированными)
  112. # последний параметр это как раз и есть признак того, что этот 
  113. подпункт раскрывается
  114. menus.addItem ("Orange", GUI_MNN_ORANGE, True, True)
  115. # теперь получаем ссылку на созданное подменю и наполняем его 
  116. новыми подпунктами
  117. # следует указать как параметр порядковый номер этого пункта меню
  118. menus_orange = menus.getSubMenu (2)
  119. menus_orange.addItem ("Orange_1", GUI_MNN_ORANGE_1)
  120. menus_orange.addItem ("Orange_2", GUI_MNN_ORANGE_2)
  121.  
  122. evt = EvtHandler ()
  123. device.setEventReceiver (evt)
  124.  
  125. # дальнейший код обычен - организуется цикл отрисовки
  126. while device.run():
  127.  driver.beginScene(1, 1, SColor(255,220,241,240))
  128.  guienv.drawAll()
  129.  driver.endScene()
  130.  
  131. device.drop


Теперь мы переходим ко второй части нашего сегодняшнего урока. Мы попробуем загрузить карту уровня игры quake3. Если у вас под руками нет дистрибутива, то ничего страшного. В поставке irrlicht идет множество примеров, необходимые файлы (изображений, звука, и других ресурсов) для которых находятся в папке media. В том числе там есть и файл map-20kdm2.pk3. Сначала я приведу пример исходного текста программы, а затем мы его проанализируем.
  1. import java
  2. import net.sf.jirr
  3. from net.sf.jirr import dimension2di
  4. from net.sf.jirr import position2di
  5. from net.sf.jirr import SColor
  6. from net.sf.jirr import vector3df
  7. from net.sf.jirr import SKeyMap
  8.  
  9. java.lang.System.loadLibrary ('irrlicht_wrap')
  10. device = net.sf.jirr.Jirr.createDevice(net.sf.jirr.E_DRIVER_TYPE.EDT_DIRECT3D9, dimension2di(640, 480), 32)
  11. driver = device.getVideoDriver()
  12. smgr = device.getSceneManager()
  13.  
  14. device.setWindowCaption("1.7 quake 3")
  15. # для того чтобы irrlicht смог бы загрузить некоторую 
  16. модель уровня quake 3 необходимо добавить ссылку на его местоположение
  17. device.getFileSystem().addZipFileArchive("E:\Program_Files_2\jirr_0.8\media\map-20kdm2.pk3", True, True)
  18.  
  19. mesh = smgr.getMesh("20kdm2.bsp")
  20. node = smgr.addOctTreeSceneNode(mesh, None, -1, 128);
  21. node.setPosition(vector3df(-1300,-144,-1249))
  22. smgr.addCameraSceneNodeFPS(None,100,500,-1,SKeyMap(),0)
  23. # прячем курсор
  24. device.getCursorControl().setVisible(False)
  25.  
  26. while(device.run()):
  27.  driver.beginScene(True, True, SColor(0,100,100,100))
  28.  smgr.drawAll();
  29.  driver.endScene();
  30. device.drop


Прежде всего, я должен указать местоположение, где находятся файлы, образующие уровень – непосредственно модель уровня и все связанные с ним ресурсы, те же текстуры. Для этого служит вызов функции addZipFileArchive, получающей на вход первым параметром путь к архиву (файлы pk3 – это обычные zip архивы ресурсных файлов игры). Второй параметр функции addZipFileArchive отвечает за возможность игнорировать регистр символов при загрузке уровня или связанных с ним ресурсов, третий же задает признак того, что можно использовать короткие имена файлов, так в архиве есть подпапки: levelshots, maps, scripts, textures. Если данный флаг не установлен (значение третьего параметра False) то при последующих загрузках ресурсов необходимо указывать путь целиком:
 device.getFileSystem().addZipFileArchive("map-20kdm2.pk3", False, False)
 mesh = smgr.getMesh("maps/20kdm2.bsp")
В случае если загрузка ресурса была не успешна (скажем уровень 20kdm2.bsp не был найден), то переменная mesh будет равна специальному значению None – ничего нет.

Затем мы присоединяем модель к специальному узлу. Дело в том, что все объекты, размещенные в виртуальном мире irrlicht, представляют собой node – узлы, различных видов. Каждый вид наилучшим образом оптимизирован для представления какой-то разновидности информации. Так есть еще узлы вида: AnimatedMeshSceneNode – для представления анимированной модели персонажа, есть BillboardSceneNode – узел для представления спрайта (плоской 2d картинки, всегда повернутой к камере лицом), есть CameraSceneNode – узел камеры и т.д.

В irrlicht узлы организованы в некоторое подобие дерева, у многих узлов есть родительский узел, один узел имеет несколько дочерних, и в свою очередь может принадлежать некоторому родительскому узлу. Вообще идея с представлением виртуального мира в виде деревьев узлов очень часто применяется, это позволяет реализовывать иерархические модификации персонажей. Например, есть узел Дед_Мороз, с дочерними узлами Мешок_Подарков, Посох. Если вы перемещается или вращаете узел Дед_Мороз, то также перемещаются и вращаются все его дочерние узлы, сохраняя относительное расстояние и ориентацию относительно своего родительского узла. Проще говоря, если ДедМороз сделал шаг влево, то его Мешок_Подарков и Посох не остались висеть на старом месте. Кроме того, грамотное проектирование иерархии узлов позволяет ускорить отрисовку 3d сцены. Дело в том, что перед непосредственно рендерингом модели происходит определение того, видим ли данный узел или нет. И если это не так, то он и все вложенные в его состав узлы не рисуются.

Следующий шаг – это смещение узла (сами модели не способы перемещаться в виртуальном пространстве – еще одна причина существования узлов). Дело в том, что начало координат уровня не совпадает с началом координат мира irrlicht. И если вы не хотите увидеть модельку уровня, где-то очень далеко в углу, то ее нужно передвинуть поближе. Конкретные значения цифр я аккуратно переписал из примера jirr, вам же в случае использования иных моделей следует воспользоваться методом “научного тыка”, или взять редактор уровней quake, например radiant, чтобы разобраться с системой координат конкретного уровня.

Теперь, когда у нас есть уровень, надо разместить камеру, глазами которой мы будем видеть мир irrlicht. Каждая камера характеризуется положением, направлением взгляда, а также широтой взгляда FOV – угол зрения. Так же важны параметры ближней и дальней плоскостей отсечения. Представьте себе камеру в виде четырехугольной пирамиды. Ее вершина – задает местоположение камеры. Центральная точка в основании – то куда мы смотрим. Угол в основании пирамиды связан с FOV. А если теперь у пирамиды-камеры обрезать вершину так чтобы получилась усеченная пирамида, то получим ближайшую плоскость отсечения. Основание же пирамиды даст нам дальнюю плоскость отсечения. Видим же мы только то, что находится в середине этой усеченной пирамиды. Да есть такая камера в irrlicht ее имя addCameraSceneNode. Есть и другая камера CameraSceneNodeFPS, использованная нами в примере, которая предоставляет встроенную возможность реагирования на действия пользователя с перемещением камеры и изменением направления взгляда. В случае же с CameraSceneNode – нам бы пришлось вручную перемещать камеру и вращать ее.

Irrlicht умеет загружать модели не только в формате quake3, но для импорта моделей объектов возможно использовать следующие известные форматы:

3D Studio (.3ds) – этот формат использовался в старых версиях 3dsmax еще во времена dos, сейчас это фактический стандарт для создания переносимых моделей во многих других приложениях 3d моделирования.

DirectX (.x) – платформо-независимый формат с поддержкой анимации персонажей.

Maya (.obj) – очень известная программа 3d-моделирования.

Milkshape (.ms3d) – формат, используемый достаточно известной программой Milkshape – есть поддержка анимации моделей.

OCT (.oct) – один из форматов, которые понимает не очень известная в нашей стране программа 3d моделирования blender. За рубежом она более популярная, хотя и проигрывает в возможностях лидерам рынка вроде 3dsmax, maya но бесплатная и большинство потребностей удовлетворяет.

OGRE Meshes (.mesh) – помните в главе посвященной обзору 3d-движков я упоминал об занимающем первое место в рейтинге devmasters движке 3d рендеринга OGRE (http://ogre3d.org). Именно в формате .mesh OGRE загружает модели в сцену.

Quake 2 models (.md2) - анимированные модели персонажей из quake 2.

В следующий раз мы продолжим работу с 3d-функциями irrlicht. Мы попробуем создавать собственные уровни и модели игрового окружения с помощью инструмента irrEdit и 3dsmax/MilkShape. Также на очереди рассмотрение средств работы с шейдерами.