{{notification.text}}

MirGames

Продолжение статьи «Околожээльные вопросы — Часть 1».

Библиотека GLU

GLU настолько часто применяется, что многими воспринимается как часть самого OpenGL. На самом деле это просто дополнительная библиотека функций, возникшая в результате опыта работы с GL. В частности GLU не поставляется вмести с драйверами видеокарт. Функции GLU обычно находятся в glu32.dll и могут быть вытащены оттуда аналогично, тому, как мы загружали ядро GL1.0 (собственно говоря, это и делается в модуле opengl.pas). В языке C обычно используются раздельные хидеры «gl.h» и «glu.h». В сети можно найти даже исходники библиотеки GLU. Вот пример функции gluOrtho2D:


procedure gluOrtho2D(left, right, bottom, top: GLDouble);
begin
   glOrtho(left, right, bottom, top, -1.0, 1.0);
end;

GLU содержит собственный механизм расширений, аналогичный GLному. Получить поддерживаемые расширения можно с помощью gluGetString(GLU_EXTENSIONS). Версию библиотеки можно получить с помощью gluGetString(GLU_VERSION). Текущая версия 1.3 (хотя вместе с Windows идет 1.2.xxx). Прочитать спецификацию и скачать последнюю версию можно на сайте http://www.opengl.org

Библиотека содержит несколько групп функций:

  • функции общего назначения, для работы с матрицами и текстурами
  • функции для работами с простыми 3D примитивами (Quadric объекты)
  • функции для тесселяции
  • функция для работы с NURBS поверхностями

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

gluLookAt, gluOrtho2D, gluPerspective, gluPickMatrix, gluProject, gluUnProject, gluScaleImage, gluBuild1DMipmaps, gluBuild2DMipmaps.

Наверняка они вам не раз встречались.

Остальные функции не такие ходовые и подробно расписаны в справке.

Работа с графическими файлами

Многие удивляются, почему OpenGL не умеет работать с различными файлами (BMP, JPG и т.п.). На самом деле все правильно, существует целая куча форматов графических файлов на целой куче целевых платформ. И не дело рисующей библиотеки опускаться на уровень функционирования операционной системы (работа с файлами) и постоянно флуктуировать вслед за развитием форматов файлов. К тому же в таком случае значительно страдает гибкость. Существует множество замечательных библиотек под любые платформы, смысл жизни которых как раз в чтении и конвертации всевозможных графических файлов. Вместе с тем никто не ограничивает использование своих собственных форматов. Хотите делать запакованные, зашифрованные, перевернутые, негативные… — пожалуйста! Да хоть рисующиеся наискосок или зигзагом. OpenGL предоставляет минимально необходимый механизм для их подключения. Вообще говоря, картинки в OpenGL можно использовать двумя способами, как растр (glBitmap, glDrawPixels) и как текстуру, которая натягивается на полигоны. Входная картинка (буду называть ее битмапом) рассматривается как массив байтов точек растра, записанных построчно (вообще в мире компьютеров все на свете – это кучка байтов, интерпретируемых тем или иным способом). OpenGL работает только с битмапами, расположенными в памяти (т.е. надо какими-то, своими средствами, загрузить картинку в память, возможно распаковать ее, преобразовать и т.п.).

Итак, этапы по обеспечению вашего приложения работой с графическими файлами

  • загрузить картинку в память (получим битмап)
  • настроить битмап (подогнать размеры, заполнить альфа-канал и т.п.)
  • загрузить битмап в текстурный объект или вывести его как растр

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

И как обычно начнем с последнего этапа.

Сначала простой способ – растровый вывод. Сделать это можно командой glDrawPixels. В качестве аргументов ей передается ширина, высота, формат растровых данных и их тип, а также указатель на собственно растровые данные. Позиция для вывода задается через glRasterPos. Обратите внимание, что растровые данные перекачиваются из системной памяти в видеоплату в каждом кадре отрисовки, что не может не сказаться на производительности. К недостаткам можно также отнести работу исключительно в 2D (никаких тебе вращений, z-координат и т.п., только 2D zoom), невозможность фильтрации, отсутствие мультитекстурирования. Вообще растровые операции по всем параметрам уступают текстурированию. В примере к данной статье видно, что картинка выводиться вверх ногами – начало растра идет в нижнем левом углу.

Основная техника — это конечно текстурирование полигонов. Ограничимся рассмотрением 2D текстур. Не буду тут подробно распинаться про использование glTexImage2D или gluBuild2DMipmaps. Остановлюсь только на нескольких моментах, важных в контексте данной статьи. А именно:

  • размер текстуры
  • форматы растра и текстуры
  • методы фильтрации
  • текстурная анимация

Текстуры должны быть размером кратным степени двойки, например 128x256. Наверное это сделано в целях оптимизации. В случае использовании glTexImage2D, мы должны сами позаботиться о соблюдении данного требования. Например, перед передачей битмапа в текстуру промасштабировать его до подходящих размеров. gluBuild2DMipmaps делает это сама (а так же создает mipmap уровни). Недавно появилось также расширение ARB_texture_non_power_of_two, которое снимает подобные проблемы, однако оно еще мало кем поддерживается.

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

Фильтрация текстур призвана улучшить визуальное качество изображения, избежать пикселяции или артефактов. Существует несколько методов фильтрации текстур различающихся качеством и напряжностью. Задается фильтрация с помощью пары команд glTexParameteri:

//нет фильтрации
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
//билинейная
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
//трилинейная
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

Билинейная фильтрация интерполирует тексели в пределах одного mipmap уровня, однако возникают визуальные артефакты при переходе между mipmap уровнями. Трилинейная фильтрация интерполирует также переходы между mipmap уровнями. Есть еще анизотропная фильтрация – это улучшение трилинейной фильтрации и реализуется через расширение EXT_texture_filter_anisotropic. Кстати, чтобы посмотреть в демках визуальные артефакты билинейной фильтрации не забудьте проверить, чтобы в настройках драйвера было выключено принудительное применение анизотропной фильтрации.

Несколько слов про текстурную анимацию. Для анимации текстур можно использовать несколько методов:

  • иметь несколько текстур (с разными фазами анимации) и периодически подменять их;
  • использовать текстурную матрицу (этот метод довольно сильно ограничен в возможностях, зато гораздо менее требователен к ресурсам), или мухлевать с текстурными координатами;
  • изменять текстуру «на лету» с помощью функций типа glTexSubImage2D (этот метод уступает по производительности).

Теперь, когда мы знаем, как запихивать битмап в OpenGL, разберемся с чтением картинки. Как обычно можно применить большое кол-во разных техник, я применю самую наипростейшую – заведу массив байтов, которую и будем считать битмапом. Для больших проектов удобнее может быть сделать класс по работе с битмапами, и даже менеджер ресурсов, все зависит от ваших потребностей. Внутренний формат в памяти возьмем 32 bpp — BGRA (так удобней потом будет использовать VCL в примерах). Итак:

type
  //Тип для хранения одного пикселя(текселя по опенжээльски). 
  TTexel = packed record
    case integer of
      0: (c: longword);
      1: (hi,lo: word);
      2: (b,g,r,a: byte);
  end;
 
  //Текстура. Пусть всегда 32 bpp.  
  TTexture = record
    width  : integer;
    height : integer;
    img    : array of TTexel;
  end;
  //Вспомогательные типы данных
  TTexelArray = array [0..0] of TTexel;
  PTexelArray = ^TTexelArray;

Вариантная запись текселя предоставляет нам доступ к различным компонентам цвета, а так же неявную упаковку значений. Например, красный цвет можно задать как t.r = 255; или t.c = $00ff0000; Динамический массив помимо прочего играет роль сборщика мусора (garbage collection). Указатель на растровые данные можно взять как @tex.img[0];

Напишем парочку функций для работы с такими текстурами.

function SetTextureSize(var tex: TTexture; w, h: cardinal): boolean;
begin
  Result:=false;
  tex.width := w;
  tex.height:= h;
  try
    SetLength(tex.img, w * h);
    Result:=true;
  except ;
  end; {try}
end;
 
function SetTextureAlphaColor(var tex: TTexture; r, g, b: byte): boolean;
var
  x, y, idx: integer;
begin
  Result := false;
  try
    //пробегаемся по каждому текселю.
    for y:=0 to tex.height-1 do begin
      idx:= y * tex.width;
      for x:=0 to tex.width-1 do begin
        //если цвет прозрачный - выставляем альфа компонент
        if (tex.img[idx].r = r) and (tex.img[idx].g = g) and (tex.img[idx].b = b) then
          tex.img[idx].a := 0
        else
          tex.img[idx].a := 255;
        //следующий элемент массива
        inc(idx);
      end; {for x}
    end; {for y}
    Result := true;
  except;
  end; {try}
end;

Таких вспомогательных функций можно написать массу — все зависит от ваших потребностей. Передача в OpenGL будет выглядеть так:


{указываем текстурный объект в который будем заносить текстуру}
glBindTexture(GL_TEXTURE_2D, K_TEX);
{загоняем растровые данные в текстуру
внутренний формат будет RGBA}
gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGBA, tex.width, tex.height, GL_BGRA, GL_UNSIGNED_BYTE, @tex.img[0]); 

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


function LoadMyImage(var tex: TTExture; filename: string): boolean;
var
  f: integer;
begin
  Result := false;
  try
    f := FileOpen(filename, fmOpenRead or fmShareDenyNone);
	if f < 0 then exit;
    FileRead(f, tex.width, sizeof(integer));
    FileRead(f, tex.height, sizeof(integer));
    SetTextureSize(tex, tex.width, tex.height);
    FileRead(f, tex.img[0], tex.width  * tex.height * sizeof(TTexel));
    Result := true;
  except;
  end;
end;  

В лучшем положении находятся пользователи VCL. В Дельфи есть встроенная поддержка BMP и JPEG форматов и еще целая куча подключается через дополнительные библиотеки (например, GraphicEx www.delphi-gems.com). Все что нам нужно, это научиться копировать растр из объекта TGraphic.

function CopyTextureFromGraphic(var tex: TTexture; img: TGraphic): boolean;
var
  b: TBitmap;
  w,h,y: integer;
  src, dst: ^integer;
begin
  Result := false;
  b := TBitmap.Create;
  try
    b.Assign(img);
    //принудительно устанавливаем формат 32bpp
    b.PixelFormat := pf32bit;
    w := b.Width; h := b.Height;
    //настраиваем размер тестуры
    SetTextureSize(tex, w, h);
    //построчно копируем растровые данные
    for y := 0 to h - 1 do begin
      src := b.ScanLine[y];
      dst := @tex.img[y * w];
      move(src^, dst^, w * 4);
    end;
    Result := true;
  except;
  end;
  if assigned(b) then b.Free;
end;

В пояснениях, возможно, нуждается только принудительная установка формата TBitmap в 32bpp после загрузки в него картинки. Этим действием мы заставляем VCL преобразовать для нас формат к удобному виду (совпадающем с нашим внутренним). После чего можем копировать строки быстрыми групповыми методами.

Заодно напишем функцию, читающую любой формат, зарегистрированный в VCL

function LoadTexture(var tex: TTexture; filename: string): boolean;
var
  p: TPicture;
begin
  Result := false;
  p := TPicture.Create;
  try
    p.LoadFromFile(filename);
    Result := CopyTextureFromGraphic(tex, p.Graphic);
  except
  end;
  if assigned(p) then p.Free;
end; 

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

На этом позвольте закруглиться с графическими файлами.

Вывод текста

Трудно представить себе приложение, где бы не понадобилось выводить текстовую информацию. Для этого существует несколько способов сделать это в OpenGL:

  • Готовыми изображениями
  • Растровыми шрифтами
  • Векторными шрифтами
  • 3D шрифтами
  • Текстурированными шрифтами

Кратко рассмотрим технику лежащую в основе этих методов, а также их достоинства и недостатки.

Метод готовых изображений

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

[+] — можно добиться высокого качества изображения надписей (красивые, с анимацией и т.п.)[+] — можно использовать не только буквы, но и любую графику [+] — высокая скорость вывода – это же просто текстурированный полигон[-] — малая гибкость – выводить можно только то, что заранее надизайнерили[-] — не подходит для вывода динамических данных

Растровые шрифты

Это, наверное, один из самых простых способов. С помощью функции wglUseFontBitmaps создаются растровые изображения (аналогичные тому, что использует glBitmap) каждой буквы и заносятся в последовательные дисплейные списки. За основу берется текущий GDI шрифт поверхности рисования. Итак, сначала создаем логический шрифт вот такой функцией:

function MakeFont(DC: HDC; name: string; size: integer; bold, italic, underline, strikeout: boolean; angle: single): HFONT;
var

  f:   LOGFONT;
begin
  fillchar(f, sizeof(f), 0);
  //определяем размер шрифта
  f.lfHeight := -MulDiv(size, GetDeviceCaps(DC, LOGPIXELSY), 72);
  //параметры начертания
  if bold then f.lfWeight := FW_BOLD else f.lfWeight:=FW_NORMAL;
  f.lfItalic := byte(italic);
  f.lfUnderline := byte(underline);
  f.lfStrikeOut := byte(strikeout);
  //остальные параметры
  f.lfOutPrecision := OUT_TT_PRECIS;
  f.lfQuality := ANTIALIASED_QUALITY;
  f.lfPitchAndFamily := VARIABLE_PITCH;
  //поворот
  f.lfEscapement := Round(angle*10.0);
  //название шрифта
  StrPCopy(f.lfFaceName, name);
 
  Result := CreateFontIndirect(f);
end;

А вот код функции создания GL растрового шрифта:

procedure CreateGLFont;
var
  DC: HDC;
  sz: SIZE;
  old, hf: HFONT;
begin
  {получаем дескриптор поверхности рисования}
  DC := wglGetCurrentDC;
 
  {создаем логический шрифт}
  hf := MakeFont(DC, K_FNT_NAME, K_FNT_SIZE, true, false, false, false, 0);
 
  {выбираем его в поверхность рисования и создаем растровый шрифт}
  old := SelectObject(DC, hf);
  wglUseFontBitmaps(DC, 0, 256, K_FNT);
 
  {можно еще например вычислить размеры текста (в пикселях)}
  GetTextExtentPoint(DC, K_STR, length(K_STR), sz);
  text_width := sz.cx; text_height := sz.cy;
 
  {подчищаем за собой}
  SelectObject(DC, old);
  DeleteObject(hf);
end;

По идее можно написать кросплатформенный аналог wglUseFontBitmaps…

Чтобы вывести текст таким шрифтом надо выполнить примерно следующие шаги:

  • задать точку начала вывода текста
    glRasterPos2f(x,y);
  • задать цвет выводимого текста
    glColor4ub(r,g,b,a);
  • задать базу дисплейных списков (фактически выбираем, каким шрифтом пользуемся)
    glListBase(fnt);
  • вызвать выполнение дисплейных списков
    glCallLists(length(s), GL_UNSIGNED_BYTE, PChar(s));

Если включить смешение цветов (GL_BLEND), то можно управлять прозрачностью текста. Для оптимизации скорости вывода текста желательно также включить альфа тест (GL_ALPHA_TEST).

[+] – относительно простые в использовании [+] – хорошая читаемость текста, особенно мелким шрифтом [-] – уступает в скорости текстурированным шрифтам[-] – не вращается, не масштабируется (простыми способами)[-] – создание шрифта не кроссплатформенное (описанный метод)[-] – затруднено взаимодействие с 3D контентом сцены[-] – размер текста зависит от разрешения экрана

Векторные шрифты

Обычно во внешней утилите готовятся данные на каждую букву шрифта. Эти данные представляют из себя вершины (x,y) отрезков из которых состоят буквы. Например, буква ‘V’ состоит из трех вершин. На этапе инициализации данные считываются, и по каждой букве подготавливается дисплейный список. Примерно так:

glNewList(fnt+char.code, GL_COMPILE);
    glBegin(GL_LINE_STRIP)
      for i := 0 to char.vertex_count - 1 do glVertex2f(char.x[i], char.y[i]);
    glEnd;
    glTranslate2f(char.offset_x, char.offset_y);
  glEndList;

Кстати, можно формировать буквы и не из последовательных отрезков.

Выводиться текст командой glCallLists, Позиционирование производиться матричными операторами. Читаемость таких шрифтов можно отнести к средним. Слишком мелкий текст искажается, слишком крупный становиться некрасивым из-за своей угловатости.

[+] – относительно простые в использовании[+] – неплохая скорость вывода[+] – прекрасно вращается и масштабируется[+] – хорошо взаимодействуют с 3D контентом сцены[-] – не очень красивые

3D шрифты

Каждый символ шрифта описывается трехмерным объектом (мешем). Можно либо самим подготавливать 3D данные для каждой буквы, либо воспользоваться функцией wglUseFontOutlines. Во втором случае, создание шрифта происходит практически аналогично тому, что я описывал для растровых шрифтов. Позиционирование вывода производиться матричными операциями. Однако этот тип шрифтов является одним из самых требовательных к ресурсам видеосистемы.

[+] – прекрасно вращается и масштабируется[+] – хорошо взаимодействуют с 3D контентом сцены[+] – доступно текстурирование, освещение и прочие спецэффекты[-] – плохая скорость вывода (да и создания)[-] – мелкий шрифт плохо читается

Текстурные шрифты

Техника используется очень простая (в описании) и очень мощная. Буква такого шрифта это не что иное, как полигон затекстурированный участком текстуры шрифта, в которой содержится изображение всех символов. Это самая распространенная методика. Текстурные шрифты прекрасно акселерируются и позволяют хорошо управлять выводом текста, как для 2D, так и для 3D приложений.

Следует обратить внимание на различие моноширинных и пропорциональных шрифтов. Например, в шрифте «Courier» все символы имеют одинаковую ширину, а в «Arial» каждая буква имеет свою ширину. Игнорирование этого факта приводит к неприятному визуальному эффекту и затрудняет читаемость текста. Поэтому, при подготовке данных шрифта, помимо фонтовой текстуры обычно создается еще файл с описанием ширины каждого символа (высота у всех одинаковая). Создание OpenGL представления (компиляция дисплейных списков) должно учитывать ширину каждого символа.

В текстуре обычно отрисовываются все символы в виде таблицы 16x16 символов, хотя это и не принципиально. Если предполагается цветовое управление выводом текста, то фонтовая текстура должна быть в сером формате (greyscale). Неплохо еще сгенерить альфа канал в текстуре (это можно сделать и при загрузке текстуры).

Большой игровой проект должен включать фонтовый движок с множеством разнообразных функций. Я ограничусь следующими:

  • загрузка данных шрифта
  • создание OpenGL шрифта
  • получение справочной информации (textWidth, textHeight)
  • подготовка к выводу текстовой информации и его завершение
  • позиционирование вывода текста
  • вывод текста с различными параметрами

Все это я поместил в несложный класс CFont. За бортом остались такие штуки, как:

  • менеджирование нескольких шрифтов
  • возможности тонкой настройки параметров шрифта (фильтрация, режимы хранения и т.п.)
  • навороченные спецэффекты
  • обработка текста (например, обработка html тэгов, выравнивание, перенос и т.п.)
  • взаимодействие с 3D контентом сцены

Данные шрифта изготовлены во внешней, самопальной утилите – это два файла font.bmp и font.dat. Формат файла font.dat наипростейший:
— сигнатура 4 байта (‘FNT1’)
— значение высоты строки – тип double
— 256 значений ширины каждого символа – тип double

Размеры заданы в текстурных координатах, а значит в диапазоне от 0 до 1.

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

  • вывод текста идет в пространстве вида
  • размеры вьюпорта в локальной системе от -1 до 1 по оси Х и от -1 до 1 по оси Y (центр (0,0), левый верхний угол (-1,-1))
  • параметры масштабирования такие, что, при размере 1.0 на вьюпорте помещается одна строка по высоте.

Настройка локальной координатной системы происходит при вызове ф-ции CFont.textBegin

procedure CFont.textBegin;
begin
  glMatrixMode(GL_MODELVIEW);
  glPushMatrix;
  glLoadIdentity;
  glMatrixMode(GL_PROJECTION);
  glPushMatrix;
  glLoadIdentity;
  glEnable(GL_TEXTURE_2D);
end;

Т.е. что я тут делаю? Я сохраняю предыдущее состояние матриц и устанавливаю свое (с glLoadIdentity, как раз и получиться та координатная система, которая мне нужна). Заодно разрешаем использование текстур (без них какой же это текстурный фонт?). Соответственно, я должен вернуть состояние матриц взад, после окончания вывода текста, иначе будет переполнение push/pop стека, а так же вывод последующей графики может разъехаться. Нет ничего проще:

procedure CFont.textEnd;
begin
  glDisable(GL_TEXTURE_2D);
  glMatrixMode(GL_PROJECTION);
  glPopMatrix;
  glMatrixMode(GL_MODELVIEW);
  glPopMatrix;
end;

Т.е. вывод текста этим методом должен быть заключен в скобки textBegin / textEnd.

Масштаб шрифта будет вычисляться так K_FNT_SCALE = 2.0/(1.0/16.0); (т.е 32). Почему так? Потому что у нас локальные координаты от -1 до 1 (2), в текстуре координаты от 0 до 1(1) и 16 строк символов.

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

Повторюсь, возможно, такое поведение вам не подойдет и придется придумывать свою схему.

В функции загрузки нет ничего необычного, кроме того, что мы сохраняем ширину каждого символа и заполнения альфа канала текстуры. Альфа канал позволит эффективно приметь альфа тест, а так же открывает возможности для управления полупрозрачнойстью текста (но это надо делать на этапе подготовки данных шрифта).

for i := 0 to tex.width * tex.height - 1 do begin
    if tex.img[i].r >= K_FNT_ALPHA_THRESHOLD then tex.img[i].a := tex.img[i].r else tex.img[i].c := 0;
  end;

Заметьте, что я исхожу из предположения, что текстура grayscale.

Интереснее посмотреть на работу функции CFont.CreateFont. У нее две задачи – запихать фонтовую текстуру в видеоплату и накомпилить дисплейных списков для каждой буквочки. Первая задача решается обычным способом, через glTexImage2D или gluBuild2DMipmaps. В условиях вывода текста разного размера, представляется целесообразным наделать mipmap уровней с трилинейной фильтрацией. Плюс к этому еще можно сэкономить на текстурной памяти, если выбрать не слишком требовательный формат хранения, например GL_ALPHA.

Вторая задача немногим посложнее — будем компилировать дисплейные для каждого символа:

  • имя дисплейного списка fnt+charnum
  • режим GL_QUADS
  • размер полигона соответствует ширине и высоте символа
  • участок текстуры, использующийся для текстурирования этого полигона также соотвествует ширине и высоте символа, а так же позиции символа в текстуре
  • после изготовления полигона мы переместим позицию вывода на ширину символа, иначе все буквы будут выводиться по одним и тем же координатам.
function CFont.createFont(tex: TTexture): boolean;
var
  i,j,k: integer;
  x, y, dx, dy: single;
begin
  Result:=False;
  //заносим текстуру в видеоплату
  glBindTexture(GL_TEXTURE_2D, K_FNT_BASE);
  {задаем трилинейную фильтрацию}
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
  gluBuild2DMipmaps(GL_TEXTURE_2D, GL_ALPHA, tex.width, tex.height, GL_BGRA, GL_UNSIGNED_BYTE, @tex.img[0]);
  //компилируем дисплейные списки
  dy := m_CharHeight;
  y := 0; k := 0;
  for j := 0 to 15 do begin
    x := 0;
    for i := 0 to 15 do begin
      dx := m_CharWidth[k];
      glNewList(K_FNT_BASE + k, GL_COMPILE);
        glBegin(GL_QUADS);
          glTexCoord2f(x, y + dy); glVertex2f(0, 0);
          glTexCoord2f(x + dx, y + dy); glVertex2f(dx, 0);
          glTexCoord2f(x + dx, y);    glVertex2f(dx, dy);
          glTexCoord2f(x, y);    glVertex2f(0, dy);
        glEnd;
        //перемещаем каретку
        glTranslatef(dx, 0, 0);
      glEndList;
      x := x + dx;
      inc(k);
    end;
    y := y + dy;
  end;
  Result := true;
end;

Может кто-то обратил внимание, что я использовал одно и то же имя для выбора текстурного объекта и в качестве базы дисплейных листов K_FNT_BASE. Просто пространства этих имен не пересекаются, а мне было лень заводить отдельную константу. Следует также отметить, что точка привязки символов является нижний левый угол. Однако удобнее привязываться к верхнему левому углу, т.к. читаем мы слева направо и сверху вниз (китайцы могут со мной поспорить). Это будет учтено в функции вывода текста. Т.е. вывод по координатам (-1,-1), нарисует текст, точно, начиная с верхнего левого угла вьюпорта и символы будут находиться ниже этой базовой линии.

Простенькая функция удаления шрифта:

function CFont.deleteFont: boolean;
var
  t: integer;
begin
  glDeleteLists(K_FNT_BASE,  256);
  t := K_FNT_BASE;
  glDeleteTextures(1, @t);
  Result := true;
end;

Вот справочные функции определения ширины и высоты текста (в наших логических координатах)


function CFont.textHeight(FontSize: single; Txt: string): single;
begin
  Result := 0;
  if Txt='' then exit;
  Result:=m_CharHeight * FontSize * K_FNT_SCALE;
end;
 
function CFont.textWidth(FontSize: single; Txt: string): single;
var
  i: integer;
begin
  Result:=0;
  if Txt='' then exit;
  for i:=1 to length(Txt) do
    Result:=Result + m_CharWidth[Ord(Txt[i])];
  Result:=Result * FontSize * K_FNT_SCALE;
end; 

С их помощью можно определять точное расположение текста на экране. Например, результат textWidth 2.0 говорит о том, что текст займет всю ширину экрана.

Наконец, подходим к кульминации – функциям вывода текста. Я поставил задачу добиться возможности последовательного вывода частей текста в одну строку без необходимости сложных вычислений позиции каждой части. Предпосылкой к этому послужило включение в состав дисплейных листов букв команды перемещения «каретки» glTranslatef(dx,0,0); Разбив вывод текста на две части – установка стартовой позиции и собственно рендеринг текста, позволяет добиться этого результата (и даже в условиях вывода наклонного текста). Как все это выглядит?

function CFont.textPos(x, y: single): boolean;
begin
  glLoadIdentity;
  glTranslatef(x, -y, 0);
end;
 
function CFont.textOut(FontSize, Angle: single; color: TTexel; Txt: string): boolean;
var
  scale: single;
  th: single;
begin
  Result:=true;
  //если выводить нечего сразу же выходим, чтобы не загружать процессор бестолковой работой
  if Txt = '' then exit;
  //попробуем избежать деления на 0
  if FontSize=0 then FontSize:=1;
 
  //вычисляем масштаб вывода
  scale := FontSize * K_FNT_SCALE;
  //вычисляем высоту строки
  th := m_CharHeight * scale;
 
  //если задан поворот, то поворачиваем
  if (Angle <> 0) then glRotatef(Angle, 0, 0, 1);
  //смещаем еще на высоту символов, потому что точка привязки - левая нижняя вершина,
  //а отрисовка идет с левой верхней.
  glTranslatef(0, -th, 0);
  //масштабируем
  glScalef(scale, scale, 1);
 
  //устанавливаем цвет, текстуру шрифта и выводим текст
  glColor4ub(color.r, color.g, color.b, color.a);
  //выбираем текстуру фонта (у нас в примере всего одна текстура и она выбрана постоянно)
  //glBindTexture(GL_TEXTURE_2D, K_FNT_BASE);
  //выводим текст
  glListBase(K_FNT_BASE);
  glCallLists(length(Txt), GL_UNSIGNED_BYTE, PChar(Txt));
 
  //восстанавливаем трансформацию матрицы, чтобы можно было выводить текст последовательно
  glScalef(1/scale, 1/scale, 1);
  glTranslatef(0, th, 0);
  if (Angle <> 0) then glRotatef(-Angle, 0, 0, 1);
end;

Все выглядит достаточно простым. Дополнительного осмысления требует примененная методика сохранить и восстановить трансформации видовой матрицы. Не проще ли было-бы применить push/pop механизм? Проще, но не правильнее. Дело в том, что нам требуется отменить трансформации масштаба, поворота и смещение базовой линии, но оставить трансформацию перемещения каретки. Это то и позволяет выводить текст последовательно.

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

[+] – хорошая скорость вывода и возможности оптимизации[+] – прекрасно вращается, масштабируется и управляются[+] – доступно текстурирование, освещение и прочие спецэффекты[+] – хорошо взаимодействуют с 3D контентом сцены[-] – читаемость мелкого текста на среднем уровне вследствие фильтрации текстур[-] – полноценный фонтовый движок достаточно сложен и требует внимания к многочисленным нюансам

На этом позвольте завершить описание методик вывода текста в OpenGL приложениях.

Вспомогательные библиотеки glut, glaux, sdl…

Перечитав кучу текста выше можно задаться вопросом «Неужели этих задач никто до нас не решал?». Решал, и очень много раз. Вспомогательных библиотек предостаточно. Большинство из них можно сравнить с концепцией «виртуальной машины». Т.е. детали взаимодействия с системой скрыты внутри библиотеки, приложению же доступны обобщенные интерфейсы. Рассмотрим несколько наиболее известных библиотек.

GLUT

GLUT одна из самых известных и простых. Реализация GLUT существует, почти для любой платформы.

Вот основные возможности и особенности GLUT:

  • менеджирование нескольких окон для OpenGL рендеринга.
  • обработка событий на основе функций обратного вызова.
  • обработка событий клавиатуры, мыши, джойстика.
  • механизм таймеров и “idle” функции.
  • средства создания меню.
  • вспомогательные функции по созданию проволочных и твердых тел (то же что и в GLU).
  • встроенный механизм растровых и векторных шрифтов.
  • различные функции по управлению окнами, включая «оверлеи».

Вот простой пример инициализации приложения:


program myapp;
uses windows, glut, unit1;
var
  cmd: array [0..255] of PChar;
  count: Integer;
  i: Integer;
begin
  //Указываем режимы и параметры отображения
  glutInitDisplayMode( GLUT_DOUBLE or GLUT_DEPTH or GLUT_RGBA);
  //задаем положение и размер окна
  glutInitWindowPosition(0, 0);
  glutInitWindowSize(640, 480);
  //переводим входные параметры в формат, понятный glut
  for i := 0 to ParamCount do cmd[i] := PChar(ParamStr(i));
  count := ParamCount + 1;
  glutInit(@count, @cmd);
  //создаем окно (glut инициализирует GL контекст)
  glutCreateWindow(‘GLUT sample’);
  //назначаем функции обратного вызова
  glutReshapeFunc(myResize);
  glutDisplayFunc(myDisplay);
  glutIdleFunc(myDisplay);
  glutKeyboardFunc(myKeyboard);
  //инициализируем необходимые GL параметры
  OneTimeSceneInit;
  //запускаем главный цикл
  glutMainLoop;
end;

Что еще добавить? Функции обратного вызова не могут быть методами класса и соглашение о вызове должно быть cdecl. При двойной буферизации переключение буферов производиться командой glutSwapBuffers; Документацию, исходники, бинарники и примеры можно найти на сайте www.opengl.org

Библиотека довольно старая (не обновляется с 1996 года) и для современных больших проектов не обеспечивает достаточной гибкости и управляемости.

GLAUX

GLAUX очень похожа на GLUT только не является кросплатформенной (windows). GLAUX Перестала развиваться еще раньше, чем glut. Набор функций немного скромнее (зато умеет грузить .bmp).

SDL

SDL сравнительно молодая и перспективная кроссплатформенная библиотека с открытым исходным кодом (www.libsdl.org). SDL покрывает большое количество типовых программистских задач:

  • работа с видеоизображением (причем не только инициализация окна);
  • работа со звуком;
  • работ с CDROM;
  • обработка большого количества типов сообщений;
  • работа с файлами (загрузка нескольких типов файлов, например .bmp .wav и некоторые другие);
  • работа с джойстиками;
  • организация и синхронизация многопоточных приложений;
  • работа с таймерами;

Помимо прочего SDL предоставляет собственный механизм вывода изображений – управление сурфейсами и блитовые операции (т.е. на SDL можно писать приложение не привязанные к GL или DX). Следует также отметить, что для SDL существует большое количество дополнительных расширений, например SDL_NET для работы с сетями, SDL_Image для работы с различными графическими файлами, SDL_TTF для работы с шрифтами TrueType и т.д.

Вот упрощенный пример инициализации приложения с помощью SDL:

var
  //главная поверхность рисования
  surf: PSDL_Surface = nil;
 
function AppCreate: boolean;
var
  icon: PSDL_Surface;
  flags: cardinal;
begin
  Result:=false;
 
  //инициализируем SDL подсистемы
  if SDL_InitSubSystem(SDL_INIT_VIDEO or SDL_INIT_NOPARACHUTE)<0 then exit;
 
  //инициализируем главное окно и задаем его размеры
  flags:=SDL_DOUBLEBUF or SDL_HWSURFACE or SDL_ANYFORMAT or SDL_OPENGL or SDL_RESIZABLE;
  surf:=SDL_SetVideoMode(640, 480, 16, flags);
  if not assigned(surf) then exit;
 
  //устанавливаем заголовок окна
  SDL_WM_SetCaption(PChar(K_APP_TITLE), nil);
 
  //загружаем и устанавливаем иконку окна
  icon:=SDL_LoadBMP(PChar('gl.bmp'));
  SDL_WM_SetIcon(icon, 0);
  SDL_FreeSurface(icon);
 
  //инициализируем GL параметры
  InitGL;
  ResizeGL(640, 480);
 
  Result:=true;
end;

В отличие от GLUT в SDL мы сами организовываем главный цикл, наподобие WndProc, причем типы сообщений покрывают практически все аспекты работы приложения (например, активация окна или пользовательские сообщения). Например так:


procedure AppMainLoop;
var
  event: TSDL_Event;
  need_quit: boolean;
begin
  //установка этого флага приведет к выходу из главного цикла,
  //и соответственно из приложения
  need_quit:=false;
  //главный цикл обработки сообщений (аналог WndProc)
  while not need_quit do begin
     //вынимаем все сообщения из очереди
     while SDL_PollEvent(@event)<>0 do begin
       //обрабатываем некоторые из событий
       case event.type_ of
 
         SDL_VIDEORESIZE : begin
           ResizeGL(event.resize.w, event.resize.h);
         end; {SDL_VIDEORESIZE}
 
         SDL_QUITEV : begin
           need_quit:=true;
         end; {SDL_QUITEV}
 
         SDL_KEYDOWN : begin
           //если нажали Esc выходим
           if event.key.keysym.sym = SDLK_ESCAPE then need_quit:=true;
         end; {SDL_KEYUP}
 
       end; {case}
     end; {SDL_PollEvent}
     //перерисовываем. Получается непрерывная отрисовка
     DrawGL;
  end; {need_quit}
end;

Помимо привычного SDL_GL_SwapBuffers, SDL позволяет загружать нестандартную GL либу (SDL_GL_LoadLibrary) и вытаскивать адреса функций (SDL_GL_GetProcAddress). Т.е. можно даже организовать свою работу с расширениями.

Подробное описание всех возможностей библиотеки потребует отдельной книжки (и, кстати, для кого писали мануалы и примеры?). В заключение лишь скажу, что несколько достаточно крупных коммерческих игровых проектов использовали SDL.

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

Пример к статье (921kb)
08.10.04 05:22