В свободное от основной работы время ваяю в области игроделания.
Последняя поделка посвящена классическому tower defence на космическую тематику:
Поскольку игра делается в изометрии, захотелось изобразить сцену примерно так

взято отсюда
Спасибо, добрый человек в профильной конференции подсказал путь поисков — DisplacementMapFilter.
Открыл документацию, скопировал пару примеров, попробовал написать свой фильтр — с ходу не получилось.
Не буду описывать свои поиски решения — сразу представлю результат (на google code) и сделаю несколько важных, на мой взгляд, пояснений.
Код рабочих классов DisplacementMapFilterGravity и DisplacementMapFilterGravityParams выкладываю как есть — т.е. на момент написания статьи, они такие-же, как и в проекте. Класс Main — для демонстрации.
class Main
class DisplacementMapFilterGravity
1. в классе Main готовлю данные для «изготовления» карты фильтра
координаты ставлю сразу для изометрии (ведь классы реальные и выдраны из рендера для изометрии)
2. после передачи параметров в DisplacementMapFilterGravity, рисую «градиентные блинчики» — заготовки для «вмятин»
получится нечто вроде (но гораздо большего размера — это я помельче изобразил, чтоб не занимать место на странице)

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

примеры других функций можно смотреть в шпаргалке, брать реализацию тут, изобретать новые
строку кода (как обращаться с easing-ами) — color = gr.func(r, deep, 0x80-deep, radius-1); — можно понять, прочитав хоть даже коротенькую статью тут же на хабре «jQuery Easing. Пользовательские easing'и»
3. после «выпекания», блинчики копируются в отдельные битмапы с использованием матричного преобразования (нужно получить их изометрическую проекцию) и помещаются общий контейнер по своим изометрическим координатам
В результате контейнер с содержимым выглядит примерно так: «блинчики» перекрывают друг-друга — нет ПЛАВНОГО перехода между ними

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

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

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

Сетка в местах деформации тоже выглядит «ломкой» — ну, надо будет спросить совета художника, можно ли как-то это спрятать. Ну а если нельзя, то, как минимум, можно использовать в прототипах — как тут, например.
Последняя поделка посвящена классическому tower defence на космическую тематику:
- дано межпланетное пространство
- траектории астероидов определяются гравитационными полями планет
- «пушечки» ракетами уничтожают астероиды
Поскольку игра делается в изометрии, захотелось изобразить сцену примерно так

взято отсюда
Встал вопрос — как?
- использование всяких фотошопов — абсолютно неприемлемо, по-понятным причинам
- прикручивать 3d движок ради рисования сетки — не очень-то и хотелось
Спасибо, добрый человек в профильной конференции подсказал путь поисков — DisplacementMapFilter.
Открыл документацию, скопировал пару примеров, попробовал написать свой фильтр — с ходу не получилось.
Не буду описывать свои поиски решения — сразу представлю результат (на google code) и сделаю несколько важных, на мой взгляд, пояснений.
Код рабочих классов DisplacementMapFilterGravity и DisplacementMapFilterGravityParams выкладываю как есть — т.е. на момент написания статьи, они такие-же, как и в проекте. Класс Main — для демонстрации.
class Main
- drawGrid() — рисует сетку и с сохраняет ее в битмапе, которая и цепляется на сцену
- updateGravities() — настраивает класс фильтра.
- добавляет две гравитационные области, по ИЗОМЕТРИЧЕСКИМ координатам (помните, когда будете экспериментировать — ведь классы выдраны из изометрического рендера)
- применяет фильтр к нарисованной в битмапе сетке
- drawPlanets() — рисует «планеты». На самом деле — это просто декорации, спроецированные на «вмятины»
class DisplacementMapFilterGravity
- createDisplacementFilter — название говорит само за себя
- createDisplacementMap — создает карту фильтра
- getShape — генерирует Shape КОНКРЕТНОЙ области гравитации
- 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. после применения фильтра получаем новую сетку:

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

Сетка в местах деформации тоже выглядит «ломкой» — ну, надо будет спросить совета художника, можно ли как-то это спрятать. Ну а если нельзя, то, как минимум, можно использовать в прототипах — как тут, например.