Как стать автором
Обновить

Вмятина в пространстве при помощи DisplacementMapFilter

Время на прочтение5 мин
Количество просмотров4.3K
В свободное от основной работы время ваяю в области игроделания.
Последняя поделка посвящена классическому tower defence на космическую тематику:
  • дано межпланетное пространство
  • траектории астероидов определяются гравитационными полями планет
  • «пушечки» ракетами уничтожают астероиды

Поскольку игра делается в изометрии, захотелось изобразить сцену примерно так


взято отсюда

Встал вопрос — как?

  • использование всяких фотошопов — абсолютно неприемлемо, по-понятным причинам
  • прикручивать 3d движок ради рисования сетки — не очень-то и хотелось

Спасибо, добрый человек в профильной конференции подсказал путь поисков — DisplacementMapFilter.
Открыл документацию, скопировал пару примеров, попробовал написать свой фильтр — с ходу не получилось.
Не буду описывать свои поиски решения — сразу представлю результат (на google code) и сделаю несколько важных, на мой взгляд, пояснений.

Код рабочих классов DisplacementMapFilterGravity и DisplacementMapFilterGravityParams выкладываю как есть — т.е. на момент написания статьи, они такие-же, как и в проекте. Класс Main — для демонстрации.

class Main
  1. drawGrid() — рисует сетку и с сохраняет ее в битмапе, которая и цепляется на сцену
  2. updateGravities() — настраивает класс фильтра.
    • добавляет две гравитационные области, по ИЗОМЕТРИЧЕСКИМ координатам (помните, когда будете экспериментировать — ведь классы выдраны из изометрического рендера)
    • применяет фильтр к нарисованной в битмапе сетке
  3. drawPlanets() — рисует «планеты». На самом деле — это просто декорации, спроецированные на «вмятины»

class DisplacementMapFilterGravity
  1. createDisplacementFilter — название говорит само за себя
  2. createDisplacementMap — создает карту фильтра
  3. getShape — генерирует Shape КОНКРЕТНОЙ области гравитации
  4. getRectangleIntersection — вспомогательный метод, находит области пересечения гравитационных полей


Теперь подробно опишу алгоритм:


1. в классе Main готовлю данные для «изготовления» карты фильтра

private function updateGravities():void
{
	var funcs:Array = [Easing.easeOutQuart];//, Easing.easeOutCubic, Easing.easeOutCircular];
	
	var arr:Array = [];
	var params:DisplacementMapFilterGravityParams;
		params = new DisplacementMapFilterGravityParams();
		params.x = 250;
		params.y = 50;
		params.radius = 200;
		params.func = funcs[arr.length%funcs.length];
	arr.push( params );
	
		params = new DisplacementMapFilterGravityParams();
		params.x = 500;
		params.y = 100;
		params.radius = 300;
		params.func = funcs[arr.length%funcs.length];
	arr.push( params );
	
        ...
}

координаты ставлю сразу для изометрии (ведь классы реальные и выдраны из рендера для изометрии)

2. после передачи параметров в DisplacementMapFilterGravity, рисую «градиентные блинчики» — заготовки для «вмятин»

private function getShape(gr:DisplacementMapFilterGravityParams):Shape
{
	var shape:Shape = new Shape();
	var g:Graphics = shape.graphics;
	var color:uint;			
	var radius:int = gr.radius;
        // TODO поэкспериментировать с отношением "глубины" к радиусу (т.е. чтоб яма была тем больше, чем больше её радиус)
	var deep:int = 10000/radius;
	
	for(var r:Number=radius-1; r>-1; r--)
	{
		color = gr.func(r, deep, 0x80-deep, radius-1);		
		
		g.beginFill(color);
		g.drawCircle(0, 0, r);
		g.endFill();
	}
	
	return shape;
}

получится нечто вроде (но гораздо большего размера — это я помельче изобразил, чтоб не занимать место на странице)



темный цвет — будущая впадина, светлый (0х80) — уровень плоскости

Тут хочу обратить особое внимание на волшебное число 0х80 — это значение цветового канала (относительно которого фильтр будет осуществлять попиксельное смещение) означает нулевой уровень искажения — фильтр НЕ БУДЕТ сдвигать пиксель с этим значением цвета. Т.е. — поскольку уровень плоскости сетки в данном случае принимается нами за ноль (цвет канала 0x80), то, чтоб края впадины плавно переходили в плоскость сетки, они тоже должны иметь это значение (0x80).

Теперь про функцию, которая в цикле дает нам подобную картинку. Это не что иное, как Easing.easeOutCircular, график которой выглядит так



примеры других функций можно смотреть в шпаргалке, брать реализацию тут, изобретать новые

строку кода (как обращаться с easing-ами) — color = gr.func(r, deep, 0x80-deep, radius-1); — можно понять, прочитав хоть даже коротенькую статью тут же на хабре «jQuery Easing. Пользовательские easing'и»

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

public function createDisplacementMap():void
{
	...

	var gr:DisplacementMapFilterGravityParams;
	for(var i:int=0; i<arrGravities.length; i++)
	{
		gr = arrGravities[i];
		
		shape = getShape(gr);
		
		var bmd:Bitmap = sprite.addChild(new Bitmap(new BitmapData(shape.width*2+2, shape.height+2, true, 0x00ff00))) as Bitmap;
			bmd.bitmapData.draw(shape, new Matrix(1, .5, -1, .5, shape.width + 1, shape.height/2 + 1));
			bmd.x = UtilsIsometric.xToIsoX(int(gr.x), int(gr.y)) - bmd.width/2;
			bmd.y = UtilsIsometric.yToIsoY(int(gr.x), int(gr.y)) - bmd.height/2;
	}
	...
}


В результате контейнер с содержимым выглядит примерно так: «блинчики» перекрывают друг-друга — нет ПЛАВНОГО перехода между ними



4. нужно сделать плавный переход между будущими впадинами. Если вкратце: берутся последовательно все «блинчики», в местах пересечений сравниваются цвета, и результирующим выбирается цвет, соответствующий большей глубине (темнее). В конце получается нечто такое:



public function createDisplacementMap():void
{
	...

	{// теперь надо проверять, есть ли наложения между слоями
		var child:Bitmap = sprite.getChildAt(0) as Bitmap;
		var rectChild:Rectangle = child.getBounds(sprite);
			bitmapdata.draw(child, new Matrix(1,0,0,1,rectChild.left-rect.left + 1, rectChild.top-rect.top + 1));
			
		var rectSumm:Rectangle = rectChild.clone();
			
		for(var l:int=1; l<sprite.numChildren; l++)
		{
			child = sprite.getChildAt(l) as Bitmap;
			
			rectChild = child.getBounds(sprite);
			
			var rectIntersection:Rectangle = getRectangleIntersection(rectSumm, rectChild);
			var bmdIntersection:BitmapData;
			if(rectIntersection != null)
			{
				bmdIntersection = new BitmapData(rectIntersection.width, rectIntersection.height, false, 0xff0000);
				bmdIntersection.draw(bitmapdata, new Matrix(1,0,0,1, -rectIntersection.left+rect.left, -rectIntersection.top+rect.top));
				
				bitmapdata.draw(child, new Matrix(1,0,0,1,rectChild.left-rect.left + 1, rectChild.top-rect.top + 1));
				
				var bColor:uint;
				var pColor:uint;
				
				var lengthW:int = bmdIntersection.width;
				var lengthH:int = bmdIntersection.height;
				for(var xx:int=0; xx<lengthW; xx++)
				{
					for(var yy:int=0; yy<lengthH; yy++)
					{
						pColor = bitmapdata.getPixel(xx+rectIntersection.left-rect.left, yy+rectIntersection.top-rect.top);
						bColor = bmdIntersection.getPixel(xx, yy);
						bitmapdata.setPixel(xx+rectIntersection.x-rect.left, yy+rectIntersection.y-rect.top, ( pColor > bColor ? bColor : pColor ));
					}
				}						
			}					
			rectSumm = rectSumm.union(rectChild);
		}

                ...
	}
}


5. в окончательном результате участки белого цвета, понятно, «закрашиваются» в тот же 0x80 (копируется все в битмап с цветом фона 0х80).

6. после применения фильтра получаем новую сетку:



Впадины выглядят непрезентабельно, ну так мы прикроем их планетами:



Сетка в местах деформации тоже выглядит «ломкой» — ну, надо будет спросить совета художника, можно ли как-то это спрятать. Ну а если нельзя, то, как минимум, можно использовать в прототипах — как тут, например.
Теги:
Хабы:
Всего голосов 5: ↑4 и ↓1+3
Комментарии6

Публикации

Ближайшие события