14 декабря 2011

Рисование текстуры "на лету"

Доброго времени суток всем читающим этот журнал! :) Удивительно хороший день сегодня, поэтому под вечер я решил создать это сообщение, содержащее в себе рассказ о небольшой хитрости, которая зачастую пригождается при программировании игр.
Когда я делал "Square is going home" меня тревожил вопрос о текстурах на объектах. Ведь от уровня к уровни размеры игровых тел могут меняться, и в голове вертелись две мысли:
  • растягивать картинку, масштабируя ее до размеров объекта
  • предварительно нарисовать десятки изображений, заготовив их под различные размеры тел
Минусы этих подходов очевидны: в первом случае при масштабировании будут видны ужасные артефакты, во втором - придется делать массу лишней работы по заготовке текстур, при этом сборка уровня превратится в бесконечную рутину по созданию картинок (если для теста нужно увеличить размер объекта на какие-нибудь 10%, придется рисовать новую текстуру).
Поэтому я выбрал программное рисование текстур для объектов и остался очень доволен результатом! То есть картинка создается "на лету" пиксель к пикселю. Сегодня я расскажу, как это можно с легкостью реализовать в GlScene.

Перед написанием этого сообщения я задумался об актуальности задачи. То есть часто ли бывают случаи, когда необходимо программно нарисовать некоторый узор на игровых объектах? И пришел к выводу, что таких случаев много:
  • линии от кнопки до двери (как в Portal'е, например)
  • можно полностью создать текстуру объекта, сделав при этом красивую обводку
  • на уникальную картинку можно добавить некий логотип клана или что-то подобное
  • зачастую необходимо вырезать картинку по трафарету (вроде собирания puzzle'ов)
Во всех этих случаях оптимальным будет создание картинки-текстуры объекта "на лету". То есть мы рисуем пиксели на будущей текстуре объекта, а затем назначаем ее объекту, переслав готовое изображение на видеокарту.
Отмечу, что в своей игре, текстуры всех игровых (физических) объектов создаются при загрузке уровня и в конечном счете это выглядит так:


Этот факт принес огромное удобство при составлении уровней - не нужно думать о том, объектами какого размера я могу наполнять уровень, ведь все картинки создаются прямо перед показом уровня. Таким образом программное рисование текстур сэкономило кучу времени.
Поначалу в голове всплывали и более простые в реализации способы:
  • можно использовать несколько спрайтов для составления одного объекта, похожим образом я делал панельку из 9ти кусков. В таком случае можно сделать красивую обводку вокруг объекта, но нельзя добиться полосатых текстур
  • можно использовать текстурные координаты, как бы вырезая из текстуры необходимый прямоугольник. Так можно сделать полосатые текстуры, но невозможно оставить обводку-свечение вокруг объектов.
  • использование Canvas'а, где можно рисовать линии, окружности и т.д. Метод безумно быстр, но пока довольно сырой - нет нормальных заливок, сглаживаний и т.д.
А потом еще добавились круглые объекты, что сподвигло меня на полный переход к программному рисованию текстур. Что-то много слов и мало кода... Немедленно исправим это положение дела!
Доступ к пикселям текстуры осуществляется следующим образом...
// объявляем необходимые переменные
bm: TGLBitmap32;
MyColor: TGLPixel32
MyWidth, MyHeight, MyAlpha: Integer;

...

// создаем нашу текстуру
bm := TGLBitmap32.Create;

// выставляем ее размеры 
bm.Width := MyWidth;
bm.Height := MyHeight;

//указываем, что она не пустая, то есть в ней будет содержимое
bm.Blank := false;

// самая главная часть - заполнение цвета и альфы пикселей
bm.ScanLine[y]^[x] :=MyColor;
bm.ScanLine[y]^[x].a := MyAlpha;

// настраиваем материал  
  with aMaterial do
  begin
    // присоединяем наше изображение, она пересылается в видеокарту
    Texture.Image.Assign(bm);

    // указываем, что используется текстура
    Texture.Disabled := false;

    // отключаем влияние источников света и тумана
    MaterialOptions := [moIgnoreFog, moNoLighting];

    // так как используем прозрачность, то выставляем соответствующие параметры смешивания цветоа
    BlendingMode := bmTransparency;
    Texture.TextureMode := tmModulate;

    // во избежание всяких примесных цветов, выставляем все в "белый"
    with FrontProperties do
    begin
      Ambient. SetColor(1,1,1, 1);
      Diffuse. SetColor(1,1,1, 1);
      Emission.SetColor(1,1,1, 1);
      Specular.SetColor(1,1,1, 1);
    end;
  end;

  // уничтожаем нашу картинку и освобождаем оперативную память (ведь сама текстура уже в видеопамяти!)
  bm.free;

С основами разобрались, теперь для заполнения простого квадрата с линейным градиентом (от sColor до eColor), можно использовать следующий код:

  dy := Round(aTexSize[3] - aTexSize[1] + 2);

  SetColor(sColor, 248, 249, 251, 255);
  SetColor(eColor, 186, 191, 187, 255);

  for i := 0 to bm.Width - 1 do
    for j := 0 to bm.Height - 1 do
      with bm.ScanLine[j]^[i] do
      begin
        ua := (j - aTexSize[1]) / dy;
        r := Round(sColor.r + (eColor.r - sColor.r) * ua);
        g := Round(sColor.g + (eColor.g - sColor.g) * ua);
        b := Round(sColor.b + (eColor.b - sColor.b) * ua);

        if (i >= aTexSize[0]) and (i <= aTexSize[2])
        and(j >= aTexSize[1]) and (j <= aTexSize[3])then
          a := Round(sColor.a + (eColor.a - sColor.a) * ua)
        else
          a := 0; 
    end;

 
Нужно отметить тот факт, что я решил использовать только pot-текстуры. А следовательно размеры прямоугольника на текстуре не обязательно совпадают с размерами самой текстуры. Так вот вектор aTexSize указывает на прямоугольник, который нужно заполнить внутри текстуры. Что попало в прямоугольник - рисуется, что не попало - убирается с помощью выставления альфы в нуль.
Дальше добавляем маленькое свечение вокруг объекта, чтобы сгладить края. Для этого определим процедуру, способную рисовать прямоугольную обводку на заданном расстоянии от краев:

  Procedure DrawAALine(d: integer);
  var
    i: integer;
  begin
    for i := Round(aTexSize[0] - d) to Round(aTexSize[2] + d) do
    begin
      bm.ScanLine[Trunc(aTexSize[1] - d)]^[i] := stroke;
      bm.ScanLine[Trunc(aTexSize[3] + d)]^[i] := stroke;
    end;

    for i := Round(aTexSize[1] - d) to Round(aTexSize[3] + d) do
    begin
      bm.ScanLine[i]^[Trunc(aTexSize[0]) - d] := stroke;
      bm.ScanLine[i]^[Trunc(aTexSize[2]) + d] := stroke;
    end;
  end; 


Где stroke - переменная типа TGLPixel32, объявленная снаружи этой процедуры. Вызывая связку из нескольких DrawAALine(), можно добиться красивых краев прямоугольника:

    SetColor(stroke, 50, 60, 40, 150);  DrawAALine(0);
    SetColor(stroke, 50, 60, 40, 40);  DrawAALine(1);
    SetColor(stroke, 50, 60, 40, 10);  DrawAALine(2);


 

Для рисования "полосатых" картинок, я заготовил всего две текстуры (по одной для каждого типа объекта), из которых копирую пиксели в итоговую картинку:

  bm.ScanLine[j]^[i] := aMatLib.Materials.GetLibMaterialByName('lines_1').Material.Texture.Image.GetBitmap32(0).ScanLine[Round(j - aTexSize[1])]^[Round(i - aTexSize[0])];
  bm.ScanLine[j]^[i].a := Round(bm.ScanLine[j]^[i].a * 0.6);


 

Итоговый код для заполнения текстур прямоугольников выглядит ужасающе:

Procedure CreateRectTexture(aMaterial: TGLMaterial; aSize, aTexSize: TVector; aObjectType: Integer; aMatLib: TGlMaterialLibrary);
var
  bm: TGLBitmap32;
  stroke: TGLPixel32;
  i, j: integer;
  dy: integer;
  sColor, eColor: TGLPixel32;
  ua: single;

  Procedure DrawAALine(d: integer);
  var
    i: integer;
  begin
    for i := Round(aTexSize[0] - d) to Round(aTexSize[2] + d) do
    begin
      bm.ScanLine[Trunc(aTexSize[1] - d)]^[i] := stroke;
      bm.ScanLine[Trunc(aTexSize[3] + d)]^[i] := stroke;
    end;

    for i := Round(aTexSize[1] - d) to Round(aTexSize[3] + d) do
    begin
      bm.ScanLine[i]^[Trunc(aTexSize[0]) - d] := stroke;
      bm.ScanLine[i]^[Trunc(aTexSize[2]) + d] := stroke;
    end;
  end;
begin
  bm := TGLBitmap32.Create;
  bm.Width := Round(aSize[0]);
  bm.Height := Round(aSize[1]);
  bm.Blank := false;
  dy := Round(aTexSize[3] - aTexSize[1] + 2);

  SetColor(sColor, 141, 202, 255, 255);
  SetColor(eColor, 108, 164, 225, 255);

  if (aObjectType = pt_SimplePhysic) then
  begin
    SetColor(sColor, 248, 249, 251, 255);
    SetColor(eColor, 186, 191, 187, 255);
  end;

  if (aObjectType = pt_StartObject) or (aObjectType = pt_FinishObject) then
  begin
    SetColor(sColor, 247, 193, 68, 210);
    SetColor(eColor, 237, 120, 3, 200);
  end;

  for i := 0 to bm.Width - 1 do
    for j := 0 to bm.Height - 1 do
      with bm.ScanLine[j]^[i] do
      begin
        ua := (j - aTexSize[1]) / dy;
        r := Round(sColor.r + (eColor.r - sColor.r) * ua);
        g := Round(sColor.g + (eColor.g - sColor.g) * ua);
        b := Round(sColor.b + (eColor.b - sColor.b) * ua);

        if (i >= aTexSize[0]) and (i <= aTexSize[2])
        and(j >= aTexSize[1]) and (j <= aTexSize[3])then
        begin
          if aObjectType = pt_AntiPlayerObject then
          begin
            bm.ScanLine[j]^[i] := aMatLib.Materials.GetLibMaterialByName('lines_1').Material.Texture.Image.GetBitmap32(0).ScanLine[Round(j - aTexSize[1])]^[Round(i - aTexSize[0])];
            bm.ScanLine[j]^[i].a := Round(bm.ScanLine[j]^[i].a * 0.6);
          end
          else
          if aObjectType = pt_AntiObjectObject then
          begin
            bm.ScanLine[j]^[i] := aMatLib.Materials.GetLibMaterialByName('lines_2').Material.Texture.Image.GetBitmap32(0).ScanLine[Round(j - aTexSize[1])]^[Round(i - aTexSize[0])];
            bm.ScanLine[j]^[i].a := Round(bm.ScanLine[j]^[i].a * 0.6);
          end
          else
            a := Round(sColor.a + (eColor.a - sColor.a) * ua)
        end
        else
          a := 0
      end;

  if aObjectType <> pt_AntiPlayerObject then
  begin
    SetColor(stroke, 0, 0, 0, 150);  DrawAALine(0);
    SetColor(stroke, 0, 0, 0, 30);  DrawAALine(1);
    SetColor(stroke, 0, 0, 0, 10);  DrawAALine(2);
  end
  else
  begin
    SetColor(stroke, 50, 60, 40, 150);  DrawAALine(0);
    SetColor(stroke, 50, 60, 40, 40);  DrawAALine(1);
    SetColor(stroke, 50, 60, 40, 10);  DrawAALine(2);
  end;

  with aMaterial do
  begin
    Texture.Image.Assign(bm);
    Texture.Disabled := false;
    MaterialOptions := [moIgnoreFog, moNoLighting];
    Texture.Disabled := false;
    BlendingMode := bmTransparency;
    Texture.TextureMode := tmModulate;
    with FrontProperties do
    begin
      Ambient. SetColor(1,1,1, 1);
      Diffuse. SetColor(1,1,1, 1);
      Emission.SetColor(1,1,1, 1);
      Specular.SetColor(1,1,1, 1);
    end;
  end;
  bm.free;
end;

Вот такой вот кошмарик получился... но мы же уже разобрали все по частям до этого - поэтому никаких сложностей в понимании быть не должно! Уточню только, что pt_AntiPlayerObject и pt_AntiObjectObject - это два вида "полосатых" объектов, для которых мы как раз копируем пиксели из заранее заготовленных картинок.
Скрин из игры:


По сути - все! На практике оказывается довольно удобной и нетрудной штукой...
Ура, товарищи! Вперед к новым вершинам!

Сообщения, схожие по тематике:

0 коммент.:

Отправить комментарий