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

August 7, 2007

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



Сегодня мы продолжаем знакомиться со средствами irrlicht. Пока мы работаем только с 2d-графикой. На этот раз мы научимся выводить на экран текстовые надписи, разумеется, с русскими буквами. Также разберем возможности модуля string – содержащего функции работы со строками.

Почти все 3d-движки, и irrlicht не исключение, перед выводом текста требуют подготовительной работы. Необходимо создать файл, в котором находятся нарисованные буквы. Алгоритм создания шрифта применительно к irrlicht вкратце таков: необходимо перебрать все символы, начиная с 32 и нарисовать их в файле bmp. Верхняя граница цикла определяется тем набором символов, который вы хотите использовать. Например, если вам нужна только латиница и цифры, то можно остановиться на 127.

Если нужна кириллица, то цикл идет по 255. Дело в том, что исторически сложилось при создании кодировочной таблицы (кодировочная таблица – это правило отображения символов в их коды или номера), отводить для латиницы нижний диапазон до 127, а верхний диапазон отводить для национальных алфавитов. Эта схема применяется только для таблиц, в которых символ кодируется однобайтовый числом. Диапазон значений байта от 0 до 255. В случае Unicode, где для кодирования символа использует 2 байта, правила игры меняются. Для тренировки откройте “пуск->стандартные->таблица символов”.

Внизу таблицы для каждого выбранного символа отображается его код в шестнадцатеричной системе счисления. Используйте стандартный калькулятор windows, в инженерном режиме, для перевода чисел из шестнадцатеричной с/с в десятичную с/с. Первый символ таблицы – восклицательный знак имеет код 33 в 10 с/с или же 21 в 16 с/с. Пока, не вдаваясь в подробности, запомните, что для irrlicht отсчет начинается именно с 32 символа (этот символ не видим).

Далее, очевидно, что для большинства шрифтов ширина символов различается так буквы “i” меньше чем буква “щ”. Поэтому для разделения графических изображений символов необходимо использовать точки специального цвета. Например, пиксель с координатами 0,0 должен быть желтым, пиксель с координатами 1,0 – красным, пиксель с координатами 2,0 – черным. Здесь мы закодировали используемые цвета для маркировки границ символов.

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

Для упрощения сего процесса я написал более продвинутую версию стандартной утилиты irrfonttool в пакете irrlicht (оригинальная утилита на основании выбранного шрифта и размера создает bmp файл с нарисованными символами). К сожалению, разработчики irrfonttool, как и многие другие англоговорящие программисты, забыли о жителях 1/6 части суши, равно как и об говорящих на китайском, фарси и других языках. Я исправил данную ошибку, добавил несколько новых возможностей, и, как же без этого, добавил пару своих ошибок. Загрузить мою версию irrfonttool можно по адресу black-zorro.jino-net.ru/irrlicht/.

Теперь следует разобраться с тем, как обрабатывать строки в python|jython. Для работы со строками вам необходимо подключить модуль string. Строки в python могут хранить не только печатные символы, но и специальные символы, например табулятор, перевод каретки. Также строка может содержать двоичные данные. Хорошая новость для любителей c|c++, в python вы можете забыть об том, что строка это массив символов с завершающим нулем терминатором.

И, следовательно, мы говорим “нет” утомительной возне с контролем за выделением и освобождением памяти. Строка может быть заключена в одинарные или двойные кавычки по вашему выбору. Если у вас есть две строки идущие друг за другом и разделенные только несколькими пробелами, то python склеит их в одну строку, например.
  1. >>> print len("a" "b")
  2. 2
Если вам необходимо создать достаточно длинную строку, содержащую большое количество как одинарных, так и двойных кавычек, также содержащую ряд символов перехода на новую строку, то рекомендуется использовать синтаксис с тремя одинарными или двойными кавычками, например, так:
  1. >>> print """ hello
  2. ... bye, go, stop
  3. ... "and" other words on few lines
  4. ... """
  5.  hello
  6. bye, go, stop
  7. "and" other words on few lines
Для определения длины строки используйте функцию len, например, так:
  1. >>> len("""
  2. ... barracuda
  3. ... and
  4. ... spring
  5. ... """)
  6. 22
Строки в python, как и во многих других высокоуровневых языках, можно объединять с помощью оператора “+”.
  1. >>> "abc" + "cde" """
  2. ... barracuda"""
  3. 'abccde\nbarracuda'
Для доступа к отдельным символам используйте оператор квадратные скобки с указанием индекса символа подлежащего извлечению. Удобно что можно задавать диапазоны символов, например “начальный_символ : конечный_символ”. Более того, каждая из этих двух частей может отсутствовать (разумеется, что не две одновременно). Не забывайте, что отсчет символов идет с нуля.

Еще один прием, это указание диапазонов с конца строки. Для этого используйте знак минус (как в последнем примере).
  1. >>> "ferrum"[1]
  2. 'e'
  3. >>> "ferrum"[3:]
  4. 'rum'
  5. >>> "ferrum"[3:5]
  6. 'ru'
  7. >>> "ferrum"[:4]
  8. 'ferr'
  9. >>> "ferrum"[:-4]
  10. 'fe'
Плохая новость при работе со строками, в том, что строки в python подобно строкам в java и в отличие от строк-массивов c|c++ не могут быть изменены. Так если я попытаюсь присвоить символу строки новое значение, то будет сгенерирована ошибка.
  1. >>> bar = "ferrum"
  2. >>> bar [5] = 'x'
  3. Traceback (innermost last):
  4.   File "<console>", line 1, in ?
  5. TypeError: can't assign to immutable object
Разумеется, переменной можно присвоить новое значение вычисленное на базе старого. В примере я заменяю третью букву переменной bar на символ “g”:
  1. >>> bar = bar [:3] + 'g' + bar[4:]
  2. >>> bar
  3. 'fergum'
Теперь рассмотрим уже знакомый нам оператор “%”. В части 3 мы использовали его для получения остатка от деления двух чисел. Применительно к строкам “%” действует подобно функции printf из c|c++. Если вы не знакомы с c c|c++, то запомните этот прием, очень полезный в ситуации, когда вам нужно вывести некоторое, достаточно длинное, сообщение пользователю. А в тексте этого сообщения должны присутствовать некоторые переменные, чередующиеся со строками. Можно пойти по классическому пути склеивания нескольких переменных. Но учитывайте, что в python не выполняется автоматического преобразования переменных к типу данных “строка”. Это нужно делать явно, что может быть достаточно громоздко.
  1. >>> money = 10
  2. >>> print "money = " + money
  3. Traceback (innermost last):
  4.   File "<console>", line 1, in ?
  5. TypeError: unsupported operand type(s) for +: 'str' and 'int'
  6. >>> print "money = " + str(money)
  7. money = 10
  8. >>> print "money = %d" % money
  9. money = 10
Перед оператором “%” находится строка-шаблон. В ней с помощью символов подстановки вы помечаете места, куда будут помещены переменные, список которых размещен после символа “%”. В моем примере была только одна переменная “money”, если же переменных несколько, то их следует разделять с помощью запятой. Комбинация символов “%d” служит для вставки целой переменной, пара символов “%f” для вставки вещественного числа, и пара символов “%s” для вставки строки.

Остальные комбинации символов подстановки вы можете найти в любом популярном учебнике по python.

Для сравнения строк вы можете применять те же операторы что и для сравнения целых или вещественных чисел. Следует помнить, что при сравнении символов регистр имеет значение.
  1. >>> print "Python" <= "jython", "Jython" == "jython"
  2. 1 0
Для того чтобы использовать следующие функции необходимо подключить к скрипту модуль string, в противном случае python не узнает вызываемые функции.

import string

Для преобразования строки в нижний или верхний регистр используйте функцию lower и upper.
  1. >>> print string.lower("BinGo") , string.upper("bAngO")
  2. bingo BANGO
Если вам нужно найти в некоторой строке подстроку, то используйте функцию find, первым параметром которой вы указываете, где будет выполнен поиск, второй параметр указывает, что именно следует найти. Возможно указать третий и четвертый параметры задающие начальную и конечную позицию подстроки которая будет извлечена из первого параметра. В случае если подстрока не будет найдена, то функция find вернет значение “-1”.
  1. >>> posAv = string.find("gravity", "av")
  2. >>> if posAv == -1:
  3. ...  print "not found"
  4. ... else:
  5. ...  print "found at " , posAv
  6. ...
  7. found at  2
Следующая функция это replace. Ее назначение заменить в некоторой строке (первый параметр) старое значение (второй параметр) на новое значение (третий параметр). Во втором примере я указал еще четвертый параметр – число, задающее количество первых совпадений которое будет подвергнуто замене. Так последняя буква “a” не была заменена на “c”.
  1. >>> string.replace("barracuda", "cud", "Gravity")
  2. 'barraGravitya'
  3. >>> string.replace("abba", "a", "c" , 1)
  4. 'cbba'
Интерес также представляют функции для выполнения преобразования строки в число. Раньше мы уже сталкивались с функциями int и float, теперь рассмотрим их подробнее. Функция int получает два параметра: первый – строковое представление числа, второй параметр – основание системы счисления. В третьем примере я пытался преобразовать строку “23” в 2 с/с. Это ошибка, т.к. в 2 с/с числа задаются только комбинацией 0 и 1.
  1. >>> print int("23", 10)
  2. 23
  3. >>> print int("23", 16)
  4. 35
  5. >>> print int("23", 2)
  6. Traceback (most recent call last):
  7.   File "<stdin>", line 1, in ?
  8. ValueError: invalid literal for int(): 23
Функция float преобразующая строку в вещественное число необязательных параметров не имеет.

Две функции, которые будут нужны нам для того, чтобы понять как работает python|jython со строками это chr и ord. Так как строка это множество символов, а каждый символ имеет свой код, то бывает полезно иметь возможность преобразовать символ в его код, а по коду вычислить символ. Давайте исследуем как работает это преобразование для python и для jython применительно к русским символам.
  1. # сначала для jython, важно исходный файл должен находиться в кодировке windows-1251
  2. import string
  3. print ord('й'), ord('а'), ord('в')
  4. print chr (ord('а') + 1)
 # результат работы скрипта
 1081 1072 1074
 Б
Теперь тоже, но для python, текст программы дополнился только указанием кодировки в которой находится файл, для windows я напоминаю это cp866. Когда я работаю в linux и пользуюсь fedora core 4, то в ней консоль работает в кодировке utf8.
  1. # -*- coding: cp866 -*-
  2. import string
  3. print ord('й'), ord('а'), ord('в')
  4. print chr (ord('а') + 1)
 # результат работы скрипта
 169 160 162
 б
  1. # -*- coding: cp1251 -*-
  2. import string
  3. print ord('й'), ord('а'), ord('в')
  4. print chr (ord('а') + 1)
 # результат работы скрипта
 233 224 226
 б
Попробуйте сами выполнить подобные вычисления для символов верхнего регистра. Теперь займемся анализом полученных данных. Результаты работы скрипта и в первом случае и во втором корректные. И там и там преобразование chr -> ord -> chr является корректным и однозначным. Отличия только в начальном значении, под которым в кодировочной таблице хранится символ “a-русское”. Опять запустите программу “пуск->стандартные->таблица символов” и рассмотрите ее подробнее.

В нижней части окна приложения вы можете выбрать используемый “набор символов”. Заметьте в какой позиции для набора “Unicode” находится символ “a-русское”, это 430 в 16 c/с или же 1072 в десятичной системе счисления, что совпадает с результатами полученными в jython.

Если же выбрать набор символов “DOS: кириллица 2”, то буква “а-русское” будет под номером A0 в 16 с/с или же 160 в 10 с/с. Что совпадает с результатами для python. Когда jython прочитал в кодировке windows-1251 файл исходного текста скрипта, то он выполнил его преобразование в unicode. Последний пример, когда файл исходного кода был в кодировке windows-1251, буква “a-маленькое” имеет код 224, что соответствует набору символов “Windows: кириллица”.

Теперь следующий шаг, запустив мою версию программы irrfonttool, укажите диапазон генерируемых символов порядка 256 (так чтобы стопроцентно захватить весь русский отрезок символов). Созданный файл bmp сохраните под любым именем. Необходимо найти на второй закладке программы позицию с которой начинается русский набор символов – это 192 для буквы A-большое и 224 – код для буквы “a-маленькое”, запомните это число. Теперь мы готовы к выводу надписей на русском в irrlicht.

Если же вы допустили ошибку при создании файла шрифта, то при запуске следующего шрифта получите сообщение вида:
 The amount of upper corner pixels and the lower corner pixels 
 is not equal, font file may be corrupted.
Пример исходного кода программы приводится ниже. Обратите внимание на существование функции adapt получающей на вход два параметра это строка текста содержащая, в том числе, русские буквы, а также число задающее номер буквы A-большое в данном шрифте. Функция adapt организует цикл по всем символам строки, проверяет каждый символ на предмет того, что он попадает в интервал русских символов. Мы выше уже узнали, что а-маленькое имеет код 1072, а символ а-большое – 1040.

Очевидно, что остальные символы русского алфавита будут иметь коды большие чем 1040. В случае необходимости коды символов корректируются на разницу в кодах для jython и шрифта. Остальной код тривиален, подобно тому как я заставлял двигаться деда мороза в части 5, здесь перемещаются строки текста. Для теста они содержат как символы нижнего, так и верхнего регистра. Кроме перемещения строк выполняется плавное изменение цвета.
  1. import java
  2. import sys
  3. import os
  4.  
  5. import net.sf.jirr
  6. from net.sf.jirr import dimension2di
  7. from net.sf.jirr import position2di
  8. from net.sf.jirr import recti
  9. from net.sf.jirr import SColor
  10.  
  11. # начало стандартное загружаем библиотеку irrlicht
  12. # создаем окно вывода
  13. java.lang.System.loadLibrary ('irrlicht_wrap')
  14. device = net.sf.jirr.Jirr.createDevice(
  15.         net.sf.jirr.E_DRIVER_TYPE.EDT_DIRECT3D9, dimension2di(800, 600), 32
  16. )
  17. device.setWindowCaption("1.3 программа - irrlicht и шрифты на русском это круто");
  18. driver = device.getVideoDriver()
  19. gui = device.getGUIEnvironment()
  20.  
  21. # это код функции выполняющей корректировку строки выводимого текста
  22. def adapt (s, start_from_in_font):
  23.     rez = ""
  24.     for _c in s:
  25.         if ord(_c) >= 1040:
  26.             rez = rez + chr( ord(_c) - (1040 - start_from_in_font))
  27.         else:
  28.             rez = rez + _c    
  29.     return rez
  30.  
  31. # теперь начинаем загружать шрифты
  32. # первым я получаю ссылку на стандартный-встроенный шрифт,
  33. # разумеется что русский текст выводимый с его помощью не работает
  34. in_font = gui.getBuiltInFont()
  35. # остальная загрузка тривиальна - нужно указать имя файла картинки с шрифтом
  36. user_font_arial = gui.getFont("f:/kolya/py/py3d/resources/fonts/arial.bmp")
  37. user_font_comic = gui.getFont("f:/kolya/py/py3d/resources/fonts/comic.bmp")
  38. user_font_baltic = gui.getFont("f:/kolya/py/py3d/resources/fonts/baltic.bmp")
  39. user_font_andale = gui.getFont("f:/kolya/py/py3d/resources/fonts/andale.bmp")
  40.  
  41. # засекаем время на начало главного цикла программы,
  42. # это нам нужно для того чтобы изменять координаты строк текста
  43. # и цвет символов
  44. initial_now_ms = device.getTimer().getTime()
  45.  
  46. while device.run():
  47.     time_now_ms = device.getTimer().getTime()
  48.     # значение текущего цвета - изменяется со скоростью 10 милисекунд = 1 градация цвета
  49.     # операция остатка от деления % очень важна, для того чтобы значение цвета не вышло за
  50.     # допустимый диапазон 0-255
  51.     cur_color = ((time_now_ms - initial_now_ms) / 10) % 256
  52.     # по аналогии расчитывается смещение выводимого текста
  53.     cur_height = ((time_now_ms - initial_now_ms) / 100) % 50
  54.  
  55.     driver.beginScene(1, 1, SColor(255,200,150,150))
  56.     gui.drawAll()
  57.     # при задании цвета первым параметром мы указываем значение альфа-компоненты,
  58.     # затем идут компоненты цвета красный, зеленый, синий
  59.     user_font_arial.draw(adapt("ARIAL: irrlicht + русские буквы = РАБОТАЕТ !", 192),
  60. 		recti(10,10+cur_height,600,80),
  61. 		SColor(cur_color,cur_color,cur_color,255))
  62.  
  63.     user_font_comic.draw(adapt("COMIC: irrlicht + русские БУКВЫ = работает !", 192),
  64. 		recti(10,60 - cur_height,600,180),
  65. 		SColor(cur_color,cur_color,cur_color,255))
  66.  
  67.     in_font.draw(adapt("INNER: irrlicht + РУсские буквы = работает !", 192),
  68. 		recti(10,110+ cur_height,600,180),
  69. 		SColor(cur_color,cur_color,cur_color,255))
  70.  
  71.     user_font_baltic.draw(adapt("BALTIC: irrlicht + рУсские буквы = работает !", 192),
  72. 		recti(10,160 - cur_height,600,180),
  73. 		SColor(cur_color,cur_color,cur_color,255))
  74.  
  75.     user_font_andale.draw(adapt("ANDALE: irrlicht + русскИЕ буквы = работает !", 192),
  76. 		recti(10,210 + cur_height,600,180),
  77. 		SColor(cur_color,cur_color,cur_color,255))
  78.     driver.endScene()