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

August 10, 2007

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



В прошлый раз мы начали знакомство с реализацией ООП в python|jython и использовали полученные знания для организации самого простого взаимодействия irrlicht с пользователем – реакции на события клавиатуры. Сегодня мы продолжаем эту тему и разберем, как обрабатывать события мыши, а также попробуем спроектировать интерфейс приложения с помощью стандартных компонентов GUI: кнопки, списки, диалоговые окна.

Задание сегодняшнего дня – разработать простой аналог mspaint, который будет уметь рисовать кривые. Вкратце идея кода такова: необходимо создать обработчик событий приходящих от мыши. Каждый раз, когда кнопка мыши зажимается, мы устанавливаем специальную переменную “флажок”. Когда отжимает кнопку мыши, то “флажок” сбрасывается. Тогда же когда мышь просто перемещается, то мы анализируем состояние флажка и рисуем линию из точки, где мышь находилась в предыдущий момент в ту точку, где она находится сейчас. Методика создания класса обработчика событий мыши абсолютно идентична той, которая использовалась для обработки событий клавиатуры в предыдущей статье.
  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 recti
  6. from net.sf.jirr import SColor
  7. from net.sf.jirr import IEventReceiver
  8.  
  9. # класс обработчика событий должен наследоваться от IEventReceiver
  10. class EvtHandler (IEventReceiver):
  11.  _driver = None # ссылка на драйвер устройства
  12.  start_paint = False # признак того идет ли рисование
  13.  old_x = 0 # координаты предыдущего положения мыши
  14.  old_y = 0
  15.  def __init__ (self, _driver):
  16.   self._driver = _driver
  17.  
  18.  def OnEvent (self, e):
  19.   # прежде всего проверяем что событие относится к мыши
  20.   if (e.getEventType() == net.sf.jirr.EEVENT_TYPE.EET_MOUSE_INPUT_EVENT):
  21.    # получаем текущие координаты мыши
  22.    x = e.getMouseInputX ()
  23.    y = e.getMouseInputY ()
  24.  
  25.    if (e.getMouseInputEvent () == net.sf.jirr.EMOUSE_INPUT_EVENT.EMIE_LMOUSE_PRESSED_DOWN):
  26.     # устанавливаем флажок - начало рисования
  27.     self.start_paint = True
  28.     self.old_x = x
  29.     self.old_y = y
  30.  
  31.    if (e.getMouseInputEvent () == net.sf.jirr.EMOUSE_INPUT_EVENT.EMIE_LMOUSE_LEFT_UP):
  32.     # сбрасываем флажок рисования
  33.     self.start_paint = False
  34.  
  35.    if (e.getMouseInputEvent () == net.sf.jirr.EMOUSE_INPUT_EVENT.EMIE_MOUSE_MOVED and self.start_paint):
  36.     # если мышь перемещается и флажок рисования установлен, то рисуем линию 
  37.     self._driver.draw2DLine (position2di(self.old_x, self.old_y), position2di(x, y), SColor (255,0,0,0))
  38.     self.old_x = x
  39.     self.old_y = y
  40.  
  41. java.lang.System.loadLibrary ('irrlicht_wrap')
  42. # драйвер я сменил с directx на opengl из за ошибок закрашивания,
  43.  почему не понятно, может баги из-за моего встроенного видео
  44. device = net.sf.jirr.Jirr.createDevice( net.sf.jirr.E_DRIVER_TYPE.EDT_OPENGL, 
  45. dimension2di(800, 600), 32)
  46. device.setWindowCaption("1.6 Mouse Events");
  47. driver = device.getVideoDriver()
  48. evt = EvtHandler (driver)
  49. device.setEventReceiver (evt)
  50.  
  51. # первый раз рисуем сцену белым цветом
  52. driver.beginScene(True, True, SColor(255,255,255,255))
  53. driver.endScene ()
  54.  
  55. while device.run():
  56.   # очень важно чтобы два первых параметра были бы False
  57.   # это значит, что не нужно стирать то, что было нарисовано раньше – историю линии
  58.   driver.beginScene(False, False, SColor(0,0,0,0))
  59.   driver.endScene ()
  60. device.drop
Теперь мы рассмотрим, как создавать GUI.
 Давайте вспомним первые наши занятия по irrlicht, там, в начале каждого файла я писал следующую строку кода:
  1. java.lang.System.loadLibrary ('irrlicht_wrap')
  2. device = net.sf.jirr.Jirr.createDevice(net.sf.jirr.E_DRIVER_TYPE.EDT_DIRECT3D9, dimension2di(800, 600), 32)
  3. driver = device.getVideoDriver()
  4. smgr = device.getSceneManager()
  5. guienv = device.getGUIEnvironment()


Ранее мы использовали только переменную driver. Объект driver умел рисовать линии, картинки из файла, выводить текст – общем все, что касается собственно 2d-рисования. Вторая и третья переменная нами никогда не использовалась. Smgr – переменная в которой хранится ссылка на менеджер сцены. Если вы решили создать настоящий трехмерный лабиринт, по которому будут бегать анимированные злодеи и стрелять спецэффектами, то SceneManager – именно то, что вам нужно. Guienv – отвечает за создание и размещение стандартных элементов управления GUI – нашей сегодняшней темы. Элементов управления на самом деле не очень много. А по сравнению с разнообразием компонентов для delpi|cbuilder/vcl/.net и их настраиваемостью, скажем прямо, irrlicht gui откровенно убог. Но с другой стороны вы же не будете использовать irrlicht для разработки офисных или финансовых приложений. Для игр и задач моделирования зачастую достаточно диалоговых окон сохранения и загрузки, пары ползунков для выбора параметров и кнопки запуска. Если же у вас возникла необходимость совместить 3d графику со сложным интерфейсом, то вам лучше подойти с обратной стороны. Разработку интерфейса можно выполнить в среде delphi|cbulder, а затем выполнить отрисовку на их форме окна irrlicht. Если вас заинтересовала эта задача, то ключевые слова для поиска: createDeviceEx – функция создания устройства irlicht с расширенным набором параметров, среди которых есть и HWND окна, на котором будет выполняться отрисовка.

К проектированию пользовательского интерфейса можно подойти с двух сторон. Первый поход – это создание элементов и их позиционирование с помощью вызовов специальных функций, а-ля, создать_кнопку, создать_меню. Второй же подход не столько упрощает проектирование интерфейса программистами, т.е. нами, сколько позволяет конечному потребителю выполнять его тонкую настройку под себя. Все серьезные приложения давно умеют менять свой вид: наборы показываемых панелей управления, их размещение и оформление. Даже игры (я говорю про хорошие игры) сейчас делают так, что выносят во внешние текстовые файлы описание положения определенных элементов интерфейса и особенности их внешнего вида. Это развязывает руки фанам разрабатывающим дополнения или моды к игре, а, следовательно, способствует лучшим продажам продукта. Этот второй подход основан на существовании двух функций irrlicht: irr::gui::IGUIEnvironment::loadGUI и irr::gui::IGUIEnvironment::saveGUI. Эти функции получают только один параметр – имя файла, содержащего описание GUI. Теперь о формате, в котором эти данные хранятся.

В общем случае существуют два противоположных полюса или концепции хранения информации. Это текстовые форматы и двоичные. У каждого из них есть свои плюсы и свои минусы. Двоичный формат хранит данные в виде максимально близком к тому виду, которым эти данные представлены в памяти компьютера. Например, если у вас есть структура данных “машинка”, с полями: модель, запас топлива, скорость …, и вы ее сохраняете в файл, который затем откроете с помощью блокнота. То боюсь, вы не сможете разделить информацию, (поля структуры будут записаны друг за другом без разделителей, длина полей будет зависеть от способа выделения памяти). Редактирование такого файла представляет собой нетривиальную задачу, и без точного знания, что именно в файле было сохранено, в каком порядке и в каком количестве, просто не возможно. Если же такая необходимость, все-таки, у вас возникла, то применяйте специальные hex-редакторы. Итак, минусы двоичных файлов – это непонятность внутреннего содержимого и не возможность его нормально редактировать без помощи специальных инструментов созданных авторами этого двоичного формата. Плюсы естественным образом вытекают из минусов, как известно “достоинства – это продолжения недостатков и наоборот”. Первый плюс – это скорость. Раз структура файла подобна структуре памяти, то чтение и запись выполняется без лишних преобразований методом прямого копирования. Также, если вы хотите чтобы никто не смог прочитать ваши файлы без помощи специального программного продукта, то двоичный формат самое то.

Текстовые форматы наоборот ориентированы на запись информации в форме максимально понятной человеку и доступны для редактирования почти любым простым текстовым редактором. В начале 2000 годов был настоящий бум универсальных текстовых форматов, их делали все кому не лень. Но дожили до сегодняшнего дня очень немногие. Прежде всего, это xml – (расширяемый язык разметки), который стал основой для множества других специальных языков. Документ xml содержит специальные теги, которые разделяют информацию на структурные части. Ниже я привожу пример простого файла xml описывающего структуру этой статьи:
  1. <?xml version=”windows-1251” ?>
  2. <article> <!—главный тег – статья содержит внутри себя три части -->
  3.    <part number=”1”>введение</part><!-- у каждой части есть атрибут: number и собственно название раздела -->
  4.    <part number=”2”>об xml и gui</part>
  5.    <part number=”3”>об обработке событий мыши</part>
  6. </article>
Именно в этом формате хранит информацию об GUI irrlicht. Кроме GUI irrlicht умеет загружать из xml файлов информацию об 3d окружении, моделях, освещении, системах частиц и т.д.

Второй известный универсальный формат yaml. Его начали разрабатывать в пику xml, как облегченную версию и не столь громоздкую. Как вариант можно хранить информацию в виде ini файлов, содержимое которых представляет набор пар: переменная = значение. Но этот формат слишком неудобен, если сохраняемая информация иерархична.

Итак, осталось только упомянуть плюсы/минусы текстовых форматов и xml в частности. Минусы очевидны – это медленная скорость работы и огромный размер файлов. Размер одной и той же информации в виде xml и в двоичном формате может отличаться в десятки раз. В настоящее время фактор избыточного размера не столь важен как раньше, но остается проблемой низкая скорость. Программа не может просто скопировать файл в память как раньше. Она должна выполнить его синтаксический разбор. Конечно, для “настоящих программистов” это не сложная задача, к тому же существует множество библиотек функций работающих с xml. Но сути дела это не меняет, скорость программ хранящих данные в xml формате часто в десятки, если не сотни раз медленнее их двоичных собратьев. Могу посоветовать только одно, разумно разделите хранящиеся данные на две категории и храните их раздельно. Также можно создать специальную утилиту, которая будет получать на вход xml документ, а на выходе будет создан двоичный файл.

При создании элементов управления мы вызываем различные функции объекта guienv. Общим для всех них является наличие параметра recti – задающего относительные координаты добавляемого элемента. Координаты задаются относительно родительского элемента, если данный параметр не указан, то считается, что компонент добавляется на высший уровень иерархии элементов окна. Очень важно для каждого элемента интерфейса задать его уникальный идентификатор. Также возможно наличие некоторых специальных параметров специфических для определенных элементов интерфейса. Например, для набора закладок таким параметром является признак рисовать или нет границу вокруг этого элемента. Для падающего списка – признак фоновой закраски области элемента. Для кнопки на инструментальной панели (ToolBar) - это изображение. Я, естественно, не могу перечислить все такие опции т.к. их очень много, но об наиболее важных я написал в примечаниях кода ниже:
  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. # это код функции выполняющей корректировку строки выводимого текста
  11. # ее код идентичен тому что мы узнали в части 7
  12. def adapt (s, start_from_in_font):
  13.  rez = ""
  14.  for _c in s:
  15.   if ord(_c) >= 1040:
  16.    rez = rez + chr( ord(_c) - (1040 - start_from_in_font))
  17.   else:
  18.    rez = rez + _c
  19.  return rez
  20.  
  21. java.lang.System.loadLibrary ('irrlicht_wrap')
  22. device = net.sf.jirr.Jirr.createDevice(
  23.         net.sf.jirr.E_DRIVER_TYPE.EDT_DIRECT3D9, dimension2di(800, 600), 32
  24. )
  25. device.setWindowCaption("1.6 GUI");
  26. driver = device.getVideoDriver()
  27. smgr = device.getSceneManager()
  28. guienv = device.getGUIEnvironment()
  29.  
  30. # далее идет объявление множества переменных, которые
  31.  будут хранить идентификаторы всех элементов управления используемых ниже
  32. GUI_TOOLBAR = 50
  33. GUI_SAVE = 51
  34. GUI_LOAD = 52
  35. GUI_EXIT = 53
  36.  
  37. GUI_PAGES_CONTROL = 100
  38. GUI_PAGES_TAB_SOURCE = 101
  39. GUI_PAGES_TAB_CHESS = 102
  40. GUI_BUTTON_APPLE = 103
  41. GUI_BUTTON_ORANGE = 104
  42. GUI_TXT_FIO = 105
  43. GUI_TXT_PHOTO = 106
  44. GUI_BUTTON_FRUITS_LIST = 200
  45. GUI_BUTTON_FRUITS_COMBO = 201
  46.  
  47. # переменная в которой хранится множество картинок
  48.  как иконки для ToolBar так и файл изображения шрифта
  49. cur_dir = "F:/kolya/py/resources/"
  50. user_font_arial = guienv.getFont(cur_dir + "book.bmp")
  51. # замещаем шрифт на новый с поддержкой русских символов
  52. guienv.getSkin().setFont (user_font_arial)
  53. # создаем элемент управления набор кнопок ToolBar
  54. # здесь и далее если я использую какую либо переменную из списка идентификаторов выше
  55. # значит что для обращения к созданному элементу управления будет 
  56. применяться именно этот идентикатор
  57. tools = guienv.addToolBar (None, GUI_TOOLBAR)
  58.  
  59. # теперь начинаем наполнять ToolBar кнопками, у кнопок нет надписи но есть картинка
  60. # метод добавить_кнопку вызывается от имени ToolBar
  61. tools.addButton (GUI_SAVE, None, driver.getTexture(cur_dir + "tbtn_save.bmp"))
  62. tools.addButton (GUI_LOAD, None, driver.getTexture(cur_dir + "tbtn_load.bmp"))
  63. tools.addButton (GUI_EXIT, None, driver.getTexture(cur_dir + "tbtn_exit.bmp"))
  64.  
  65. # теперь добавляем набор закладок, параметр номер 2 задает ссылку на
  66. # родительское окно, так я указал специальное значение None, что значит
  67. # элемент добавляется на высший уровень иерархии
  68. pages = guienv.addTabControl (recti (0, 30, 980, 590), None, False ,True, GUI_PAGES_CONTROL)
  69. # и добавляем две закладки, также обратите внимание что вызов методов addTab идет уже не от имени
  70. # guienv а именно от имени TabControl
  71. tab_source = pages.addTab ("Tab Caption")
  72. tab_chess  = pages.addTab (adapt("Шахматы", 192))
  73.  
  74. # создаем различные элементы управления
  75. # сначала создаем две кнопки
  76. # последний параметр это
  77. btn_apple  = guienv.addButton(recti(10,10,300,30), tab_source, GUI_BUTTON_APPLE, "Apple")
  78. btn_orange = guienv.addButton(recti(10,50,300,80), tab_source, GUI_BUTTON_ORANGE, adapt("Апельсин", 192))
  79. # затем список
  80. lst_fruits = guienv.addListBox(recti(10,100,300,160), tab_source, GUI_BUTTON_FRUITS_LIST)
  81. # теперь выпадающий список
  82. combo_fruits = guienv.addComboBox(recti(10,180,300,204), tab_source, GUI_BUTTON_FRUITS_COMBO)
  83. # создаем текстовое редактируемое поле
  84. txt_fio = guienv.addEditBox("Vasyan Tapkin", recti(10,220,300,244), True, tab_source, GUI_TXT_FIO)
  85. # и наконец элемент управления "Изображение"
  86. image_foto = guienv.addImage(recti(10,260,310,560), tab_source, GUI_TXT_FIO)
  87. # загружаем в созданный элемент файл картинки
  88. image_foto.setImage (driver.getTexture(cur_dir + "portret2.jpg"))
  89.  
  90. # и наполняем список и падающий список элементами - текстовыми строками
  91. captions = ("Apple", "Orange", "Grapes")
  92. for i in captions:
  93.     lst_fruits.addItem (i)
  94.     combo_fruits.addItem (i)
  95.  
  96. # дальнейший код обычен - организуется цикл отрисовки
  97. while device.run():
  98.     driver.beginScene(1, 1, SColor(255,220,241,240))
  99.     smgr.drawAll()
  100.     # именно здесь выполняется отрисовка всех тех элементов которые были созданы выше
  101.     guienv.drawAll()
  102.     driver.endScene()
  103. device.drop


Важное замечание: в примере был загружен пользовательский шрифт, с помощью которого мы выводим надписи на кнопках, закладках и падающих списках. Примененная методика идентична той, которую мы применяли в части 7. Обработка русских символов корректно работает как в выводе статических надписей (падающий список и заголовки кнопок так и динамических: текстовые поля в которые можно вводить русские символы). Следует, однако, уточнить, что материал данной статьи ориентируется на irrlicht 1.1, буквально месяц назад вышла новая версия irrlicht 1.3 в которой сильно были изменены, переписаны и переделаны (без поддержки обратной совместимости) методы работы со шрифтами. Пока еще нет (по крайней мере, на момент написания данной статьи) версии jirr поддерживающей работу с irrlicht 1.3. Но как только обновленные биндинги irrlicht -> java выйдут, я настоятельно рекомендую не затягивать и срочно переходить под новую версию irrlicht. В частности методы чтения/записи описания GUI в xml файлы доступны только под 1.3. Примеры, как выглядят эти файлы xml, я не привожу, т.к. они достаточно громоздки. Стоит отметить, что в irrlicht 1.3 появился визуальный редактор GUI интерфейсов. В идеале вы таскаете на рабочую область компоненты, настраиваете их положение и внешний вид, а затем сохраняете во внешнем xml файле (эдакий delphi для бедных). Увы, но сказать по правде, более “глючного” программного продукта я не видел уже давно. Оправдывает разработчиков irrlicht только то, что эти возможности пока экспериментальные.

В следующий раз мы закончим работу с обработкой событий от элементов GUI и закроем большой подраздел посвященный работе с 2d графикой. Постарайтесь получить пару уроков по 3dsmax и заодно раздобудьте quake2|quake3. Мы попробуем создать приложение, загружающее карту уровня этой игры.