В свободное от основной работы время ваяю в области игроделания.
Последняя поделка посвящена классическому 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. после применения фильтра получаем новую сетку:

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

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