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

August 9, 2007Comments Off on Программируем трехмерную графику с Irrlicht . Часть 9

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



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

Также тема сегодняшней статьи – классы. В прошлый раз я поставил перед нами цель научиться работать со сложными структурами данных. Списки|tuple позволили нам группировать множество переменных в единое целое. Но остается вопрос об еще более сильном объединении данных и, это важно, методов их обработки. Если вы вспомните предыдущий пример с машинкой, то следующим шагом после взаимосвязи значений множества переменных, которые образуют ее (цвет, скорость, запас топлива), будет четкое определение операций или действий, которые можно выполнять с этой машинкой – структурой данных. Так машинка может ездить и перевозить грузы, но не может плавать или же выдавать деньги подобно банкомату.

Опустить в изучении любого языка программирования классы невозможно по причине того, что в современной индустрии разработки ПО роль философского камня играет фраза “повторное использование кода”. Методика программирования позволяющая использовать ранее созданный код в новых проектах, дает вам ряд плюсов.

Скорость (скорость достигается как за счет уменьшения времени написания кода, так и уменьшения затрат на отладку и тестирование приложения).

Рациональное использование человеческого ресурса. В перспективе модули (библиотеки, компоненты, классы) разработанные настоящими профессионалами могут использоваться новичками, не особенно понимающими, как именно работает некоторая библиотека или модуль. Великий гуру Joel Spolsky написал ряд статей раскрывающих секреты в области программирования/проектирования и управления командой разработчиков http://russian.joelonsoftware.com/. Эти жизненные истории лучше, чем многие из псевдонаучных книжек по управлению разработкой ПО доступных в наших книжных магазинах.

Надежность или качество. Раз мы используем чужие или собственные наработки многократно, то мы не изобретаем очередной велосипед, и можем тратить больше времени на улучшение качества проекта. Разумеется, что для того, чтобы писать повторно используемый код, мы тратим больше времени на планирование и тестирование. Мы должны писать максимально универсальный код (в случае часто меняющихся или неясных требований такой подход не всегда подходит).

Разумеется, что повторное использование кода - это не метод “копировать - вставить”. Такой подход принесет вам больше вреда, чем пользы. Методики объектно-ориентированного программирования/проектирования являются необходимыми, но не достаточными для достижения “повторного использования кода”. Подробнее об современных технологиях программирования вы можете почитать, найдя литературу по ключевым словам: паттерны, антипаттерны, RUP, data driven development, test driven development, uml, agile программирование, экстремальное программирование.

Вкратце, что такое класс? Класс - это штука похожая на типографскую матрицу. На матрице отпечатаны типовые заголовки, изображения. На основании класса создается объект – это отпечаток этой матрицы. В отличие от типографии, объекты могут изменяться после создания, но классы изменяться не могут. Поэтому нужно потратить множество сил на то, чтобы грамотно спланировать модель классов. Слово “модель” очень важно, под моделью мы понимаем некоторое упрощенное до той или иной стадии подобие окружающего нас мира. Например, если вам нужно создать приложение учета домашней библиотеки, то вы можете выделить классы книжный шкаф, который содержит некоторое количество объектов типа класс-полка. На полке, в свою очередь, находятся множество объектов созданных на основе типовых матриц: книга, журнал, газета. Класс это следующий шаг в развитии понятия пользовательских типов данных. Встроенные типы данных – спланированы авторами языка (числа целые и вещественные, логический тип данных, строки). Очевидно, что предусмотреть встроенные типы данных для всех ситуаций на свете не возможно (так нет в python такого встроенного типа данных, как “Машинка”). Поэтому необходимо дать программисту способ создавать такие типы данных, которые ему подходят лучше всего для конкретных задач. Самый простой способ это создать группу из двух, трех, … ста простых переменных – это были списки и tuple. Более интересно объединить данные и операции, которые можно выполнять с ними. Еще интереснее построить иерархию наследования/зависимости одних пользовательских типов данных от других. Неплохо обеспечить защиту данных от несанкционированного использования, а также унифицировать операции над сходными типами данных. Мои слова могут показаться вам очень неконкретными, но привыкайте оперировать и мыслить абстрактно. В замечательном сборнике “физики тоже шутят” есть отличная история.
 Давида Гильберта (1862—1943) спросили об одном из его бывших учеников.
 — Ах, этот то? — вспомнил Гильберт. — 
 Он стал поэтом. Для математики у него было слишком мало  воображения.
Каждый класс/объект состоит из произвольного количества переменных, как встроенных, так и созданных нами. Эти переменные мы называем полями или атрибутами. Также класс содержит некоторое количество функций – их мы называем методы. Мы можем создавать классы наследуя их от других классов. Наследование означает то, что каждый экземпляр класса Son производного от класса Father будет содержать не только свои собственные уникальные методы и поля, но и те которые были у родительского класса. Если от класса Son породить еще один класс, то этот наследник будет содержать свои методы/поля, равно как и методы/поля классов Son и Father.

Ключевое слово class служит для объявления нового класса. Вторая строка – произвольный текст внутри тройных кавычек – так называющая документирующая строка. Это что-то вроде комментария. Но, в отличие от комментариев в большинстве традиционных языков, которые доступны только читающему исходный код программисту, а в итоговой (скомпилированной) версии программы удаляются и не доступны, документирующие комментарии python|jython сохраняются. Так, что вы в любой момент можете запросить подсказку об некотором классе или его поле/методе используя вызов функции help или __doc__, например, так:
  1. # -*- coding: cp1251 -*-
  2. import math
  3. import sys
  4.  
  5. class Car:
  6.     """ этот класс представляет машинку """
  7.     speed = 0
  8.     # эта переменная кодирует скорость машинки
  9.     fuel = 0
  10.     # а эта переменная кодирует запас топлива
  11.     coord_x =0
  12.     coord_y =0
  13.     # эти две переменные кодируют местоположение машинки
  14.  
  15.     def __init__ (self, _x, _y, _f):
  16.         """ это конструктор класса """
  17.         self.speed = 0
  18.         self.coord_x = _x
  19.         self.coord_y = _y
  20.         self.fuel = _f
  21.  
  22.     def moveTo (self, angle, v, time):
  23.         """ эта функция выполняет перемещение машинки в заданном
  24.  направлении в течении заданного времени и скорости """
  25.         if (self.fuel < v * time * 0.1):
  26.             sys.exit("ужасная ошибка, топлива не хватит на перемещение
  27.  машинки в течении заданного времени %f со скоростью %f" % (time, v))
  28.         self.speed = v
  29.         self.coord_x += v*time*math.cos(angle)
  30.         self.coord_y += v*time*math.sin(angle)
  31.         self.fuel -= v * time * 0.1
  32.         # здесь 0.1 - это коэффициент расхода топлива на 1 километр пути
  33.  
  34.     def printMe (self):
  35.         """ эта функция распечатывает сведения об текущем положении и состоянии машинки """
  36.         print "\ncar coords (x,y) = (%f,%f)" % (self.coord_x, self.coord_y)
  37.         # это конечно идеальный пример без учета трения
  38.         print "car speed (v) = (%f) and fuel (f) = (%f)\n" % (self.speed, self.fuel)
  39.  
  40.  
  41. car_1 = Car (10, 20 , 200)
  42. help (Car) # распечатываем справку по всему содержимому класса
  43. print Car.__doc__ # распечатываем справку только по назначению класса
  44. print car_1.__doc__ # аналогично предыдущему примеру
  45.  
  46. car_1.printMe () # распечатываем информацию об состоянии машинки
  47. car_1.moveTo (0.1, 70, 60) # выполняем перемещение машинки на 
  48. огромное расстояние, здесь у нас должно кончиться топливо
Теперь разберем код примера. Начало тривиальное - идет подключение модулей math (в нем нам потребуются функции sin, cos), а также модуля sys (нам нужна функция exit, которая завершает работу всего приложения, выводя на экран строку текста, переданную как параметр этой функции).

После объявления класса идет документирующий комментарий, поясняющий назначение класса. Затем идет перечисление переменных в составе класса: координаты, скорость, запас топлива. Методы класса объявляются также как и обычные функции, но есть небольшое отличие. Очевидно, что метод класса всегда вызывается от именно какого-то объекта класса. В самом конце примера я вызываю метод printMe, указывая перед этим именем имя переменной хранящей объект, затем символ точки (помните, что он означает неявное слово “внутри”), и, наконец, имя самого метода с параметрами. Когда метод класса объявляется, он всегда получает в качестве первого параметра особое значение self, – это переменная, внутри которой хранятся все поля/переменные текущего экземпляра класса.

Функция moveTo получает три параметра – скорость движения, время движения и угол вектора задающего направление движения. В теле метода, перед тем как изменить координаты и расход топлива, выполняется проверка, хватит ли текущего запаса топлива. В этом еще одно важное свойство ООП – централизованное изменение переменных. Это позволяет всегда контролировать данные на предмет их логической непротиворечивости. Если бы программист изменял бы переменные без специального метода moveTo, то ему всегда требовалось помнить, что нужно выполнить проверки на корректность данных. Так, в примере я забыл добавить проверку на возможное отрицательное значение скорости или времени. К счастью, когда я нашел эту ошибку, мне нужно добавить пару проверок только в одном месте – теле метода класса moveTo. А если бы такой централизации не было, то мне пришлось бы найти абсолютно все места, где выполняется несанкционированный доступ к переменным состоянии машинки, и исправлять их. Как вывод, ООП дает нам возможность централизовать код.

Кроме метода moveTo и printMe есть еще один необычный метод, имя которого “__init__”. Это так называемый конструктор. Зачем нужны конструкторы? Здесь название говорит само за себя. Конструктор конструирует объект на основании типовой матрицы-класса. Если бы конструкторов не было бы, то все объекты были бы идентичны друг другу. По-крайней мере, до того как мы изменили бы некоторые поля объекта, вызвав некоторые методы класса. Но разве, не логичнее было бы совместить создание объекта и его настройку – присвоение специальных значений полям. Конструктор может получить любое количество переменных и вычислить на основании их с помощью условных конструкций или циклов значения для полей класса. Конструктор неявно вызывается, когда я пишу:
 car_1 = Car (10, 20 , 200)
Для изучения методов работы с обработкой событий клавиатуры irrlicht нарисуем какой-нибудь красивый фрактал. По нажатию на клавишу P будет создаваться скриншот картинки, клавиши WASD будут использованы для перемещения изображения, а клавиши ZX – для масштабирования изображения. Общие сведения о фракталах можно получить по адресу http://ru.wikipedia.org/wiki/Фракталы. Нас же более интересует следующая цитата: “В компьютерной графике фракталы используются при создании изображений сложных, похожих на природные, объектов: например, облаков, снега, мусорных куч, береговых линий и др.”. Для построения множества Мандельброта воспользуемся следующим алгоритмом:

Для всех точек на комплексной плоскости (разумеется, только тех которые непосредственно попадают на видимый нами экран) вычисляем достаточно большое количество раз значение некоторой формулы. Обычно используют: z = z^2 + c, (здесь c – некоторое комплексное число, зависящее от текущей координаты точки экрана, равно как и z – комплексное число вначале равное нулю). Каждый раз в ходе этого цикла мы проверяем абсолютное значение z. Если это значение больше максимального, то рисуем точку с цветом, вычисляемым по количеству итераций, иначе рисуем точку черного цвета.

Черный цвет в середине будущего рисунка показывает, что в этих точках функция стремится к нулю - это и есть множество Мандельброта. За пределами этого множества функция стремится к бесконечности. А самое интересное - это границы множества. Они то и являются фрактальными. При увеличении заметно самоподобие фигуры – одна из основных черт фракталов, бесконечное самоповторение. Непосредственно работать с комплексными числами мы не можем, к счастью операция z = z^2 + c, легко переводится в z = (xz + i*yz) * (xz + i*yz) + (xc + i*yc) = xz^2 – yz^2 + xc + i*(2*xz*yz + yc).
  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 IEventReceiver
  7.  
  8.  
  9. class EvtHandler (IEventReceiver):
  10.   """Этот класс служит для обработки событий,
  11.    которые генерируются средой irrlicht"""
  12.   _driver = None #ссылка на видеодрайвер
  13.   numOfScreens = 0
  14.   #переменная хранящая количество сделанных скриншотов,
  15.  
  16.   koeff = 0.008 # коэффициент увеличения фрактала
  17.   max_length = 1000  # максимальное удаление в квадрате 
  18. для исключения *дорогой* операции извлечения корня
  19.   max_iterations = 16 # предельное количество итераций
  20.   koeff_colors = 256 / max_iterations # для ускорения расчета цветовых градаций
  21.  
  22.   delta_x = 0 # величина смещения рисуемой фигуры
  23.   delta_y = 0
  24.  
  25.   def __init__ (self, _driver):
  26.   IEventReceiver.__init__ (self)
  27.   # вызвали конструктор родительского класса
  28.   self._driver = _driver
  29.  
  30.   def OnEvent (self, e):
  31.   if e.getEventType() == net.sf.jirr.EEVENT_TYPE.EET_KEY_INPUT_EVENT and (not e.isKeyInputPressedDown()):
  32.     if e.getKeyInputKey() == net.sf.jirr.EKEY_CODE.KEY_KEY_P:
  33.     # имя файла скриншота зависит от порядкового номера 
  34.     self._driver.writeImageToFile (  self._driver.createScreenShot (), "screen_" + str(self.numOfScreens) + ".tga")
  35.     self.numOfScreens = 1 + self.numOfScreens
  36.     # анализ клавиш перемещения и масштабирования
  37.     if e.getKeyInputKey() == net.sf.jirr.EKEY_CODE.KEY_KEY_W:
  38.     self.delta_y += 0.1
  39.     if e.getKeyInputKey() == net.sf.jirr.EKEY_CODE.KEY_KEY_S:
  40.     self.delta_y -= 0.1
  41.     if e.getKeyInputKey() == net.sf.jirr.EKEY_CODE.KEY_KEY_A:
  42.     self.delta_x -= 0.1
  43.     if e.getKeyInputKey() == net.sf.jirr.EKEY_CODE.KEY_KEY_D:
  44.     self.delta_x += 0.1
  45.     if e.getKeyInputKey() == net.sf.jirr.EKEY_CODE.KEY_KEY_Z:
  46.     self.koeff += 0.001
  47.     if e.getKeyInputKey() == net.sf.jirr.EKEY_CODE.KEY_KEY_X:
  48.     self.koeff -= 0.001
  49.   return True
  50.  
  51.   def Paint (self):
  52.     driver.beginScene(1, 1, SColor(255,0,0,0)) # вначале все поле закрашивается черным цветом
  53.     pos_x =  - 160
  54.     while pos_x <= 160:
  55.     pos_y =  - 120
  56.     while pos_y <= 120:
  57.       k = 0 # количество итераций необходимых для превышения предельного расстояния
  58.       zx = 0 # начальное комплексное число всегда равно нулю
  59.       zy = 0
  60.       cx = pos_x * self.koeff + self.delta_x
  61.       cy = pos_y * self.koeff + self.delta_y
  62.  
  63.       lengthof = zx*zx + zy *zy
  64.       while (lengthof < self.max_length) and (k < self.max_iterations):
  65.       new_zx = zx*zx - zy*zy + cx
  66.       new_zy = 2 * zx * zy + cy
  67.       zx = new_zx
  68.       zy = new_zy
  69.       k = 1 + k # увеличивем счетчик количества итераций
  70.       lengthof = zx*zx + zy *zy
  71.       if (k < self.max_iterations): # лишь в том случае если мы
  72.  вышли за пределы круга за меньшее чем максимальное количество 
  73. итераций мы рисуем некоторый цвет, иначе оставляем фоновый черный
  74.       self._driver.draw2DLine (position2di (pos_x + 160, pos_y+120), position2di (pos_x +161, pos_y+120),
  75.              SColor (255, k*self.koeff_colors , 256 -  k*self.koeff_colors , 256 - k*self.koeff_colors)
  76.         )
  77.       # рисуем точку из линии длиной в 1 пх
  78.       pos_y  = 1 + pos_y
  79.     pos_x  = 1 + pos_x
  80.     driver.endScene()
  81.  
  82. java.lang.System.loadLibrary ('irrlicht_wrap')
  83. device = net.sf.jirr.Jirr.createDevice(
  84.   net.sf.jirr.E_DRIVER_TYPE.EDT_DIRECT3D9, dimension2di(320, 240), 32
  85. )
  86. device.setWindowCaption("irrlicht");
  87. driver = device.getVideoDriver()
  88. evt = EvtHandler (driver)
  89. device.setEventReceiver (evt)
  90. while device.run():
  91.   evt.Paint ()   
  92.  
  93. device.drop


Как задание вам необходимо ускорить работу программы, выполняя перерасчет/перерисовку только тогда, когда пользователь изменил какой-то из параметров множества Мандельброта. В следующий раз мы познакомимся с методиками создания пользовательских интерфейсов в irrlicht с помощью кнопок, текстовых полей, выпадающих списков.