Доброго времени суток всем читающим этот журнал! :) Удивительно хороший день сегодня, поэтому под вечер я решил создать это сообщение, содержащее в себе рассказ о небольшой хитрости, которая зачастую пригождается при программировании игр. Когда я делал "Square is going home" меня тревожил вопрос о текстурах на объектах. Ведь от уровня к уровни размеры игровых тел могут меняться, и в голове вертелись две мысли:
Поэтому я выбрал программное рисование текстур для объектов и остался очень доволен результатом! То есть картинка создается "на лету" пиксель к пикселю. Сегодня я расскажу, как это можно с легкостью реализовать в 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 коммент.:
Отправить комментарий