
Растеризация MovieClip в последовательность BitmapData
Есть известный прием для оптимизации скорости отрисовки векторной анимации на экране, а именно: перегонка мувиклипа в последовательность битмапдат.
Последние дни обнаружилось, что реализация этого приема вызывает некоторые одинаковые трудности даже не у одного камрада. В частности появляется дрожание. Поэтому, после первого обращения я решил позже выложить свою реализацию. А после второго решил, что плохо медлить :)
UPD: Расширил черновую битмапу, чтобы учесть возможные заступы из-за фильтров.
UPD 2: getColorBoundsRect оказался излишне медленным, когда он работает на большой битмапе (спасибо Эду Рыжову, который это заметил). То-есть, когда черновая битмапа становится, скажем 1024 на 1024, из-за большой картинки, а последующие картинки рендерятся маленькие, то сильно теряется время. Но без getColorBoundsRect обойтись нельзя, если вы используете маски. Я попробовал каждый раз пересоздавать черновую битмапу и оказалось, что выделение памяти настолько быстро, что общее время теперь не сильно отличается от времени без использования getColorBoundsRect вообще. На моем текущем проекте старый вариант занимал 2400ms, новый 700ms, без использования черновой битмапы и getColorBoundsRect 685ms. Обновил код и инструкцию.
Итак, ноги у всего этого растут от класса, описанного тут: touchmypixel о кешированных анимациях.
Из описанного класса мне необходима была только хитрость по получению прямоугольника клипа, с учетом его центра. А именно clip.getBounds(clip). Остальное в образце было сделано неоптимально и бажно. В частности, использовался не getBounds, а getRect, который не учитывает толщину линиий и поэтому обрезает края клипа иногда больше чем на один пиксель. Кроме того, память BitmapData использовалась очень неоптимально, необходимы еще были дополнительные прямоугольники в клипе, сделанные вручную.
Поэтому я сделал свой класс, который имеет следующие преимущества:
1. Вычисляется размер и координаты для каждого кадра анимации. Это позволяет очень сильно экономить память и не гонять по экрану битмапы, которые больше размером чем надо. В частности, если у вас есть анимация прыгающего мячика, или, как у меня, анимация взлетающей ракеты, то часть кадров будут полупустыми, поскольку размер прямоугольника будет захватывать и ракету в верхней части траектории и внизу.

У меня каждый кадр занимает ровно столько, сколько нужно, при этом сохраняются относительные координаты, так что анимация будет проигрываться в точности.

2. Я добавляю один пиксель по краям на всякий случай, поскольку оказалось, что и метод getBounds может не учесть сглаживание, и обрезать полоску в один пиксель. Более того, getBounds, к сожалению, не учитывает фильтры. То-есть, если у вас наложен очень сильный blur или glow — getBounds вернет прямоугольник без учета этого, и может обрезать очень много пикселей. В своих играх я с этим пока не сталкивался. Можно ставить в этих редких случаях почти невидимые точки по краям в клипе, чтобы оконтурить нужный размер. В любом случае, эта проблема пока изящно не решается в любой реализации. Если кто что нароет — напишите.
3. После рендеринга в BitmapData я все равно еще делаю обрезание возможных пустот. Дело в том, что если в предыдущем случае getBounds не учитывал фильтры, то в случае маскирующих слоев — он наоборот, учитывает все что невидимо. То-есть, если вы используете слой маску, то getBounds при вычислении оконтуривающего прямоугольника учитывает даже невидимые области. Вот, например, на рисунке справа четыре кадра нужной мне анимации. Однако в клипе она организована с помощью маски-слоя. Поэтому обычный алгоритм реализует четыре гигантских битмапы с большими пустотами сверху и снизу. Я для финальной обрезки использую удобный метод getColorBoundsRect.
Код будет внизу поста, а здесь немного по его использованию:
а). Чтобы получить результат, необходимо просто вызвать статический метод
б). В полученном BmpFrames в массиве лежат BitmapData кадры. Отдельно лежат координаты для сдвига, чтобы кадры лежали в нужных местах. Например, если вы потом будете пихать кадры в Bitmap, то просто не забудьте присвоить эти координаты его x и y.
В частности, у себя в классе анимации я имею один Bitmap в котором каждый кадр меняю битмапдату из массива и корректирую координаты.
Код:
Последние дни обнаружилось, что реализация этого приема вызывает некоторые одинаковые трудности даже не у одного камрада. В частности появляется дрожание. Поэтому, после первого обращения я решил позже выложить свою реализацию. А после второго решил, что плохо медлить :)
UPD: Расширил черновую битмапу, чтобы учесть возможные заступы из-за фильтров.
UPD 2: getColorBoundsRect оказался излишне медленным, когда он работает на большой битмапе (спасибо Эду Рыжову, который это заметил). То-есть, когда черновая битмапа становится, скажем 1024 на 1024, из-за большой картинки, а последующие картинки рендерятся маленькие, то сильно теряется время. Но без getColorBoundsRect обойтись нельзя, если вы используете маски. Я попробовал каждый раз пересоздавать черновую битмапу и оказалось, что выделение памяти настолько быстро, что общее время теперь не сильно отличается от времени без использования getColorBoundsRect вообще. На моем текущем проекте старый вариант занимал 2400ms, новый 700ms, без использования черновой битмапы и getColorBoundsRect 685ms. Обновил код и инструкцию.
Итак, ноги у всего этого растут от класса, описанного тут: touchmypixel о кешированных анимациях.
Из описанного класса мне необходима была только хитрость по получению прямоугольника клипа, с учетом его центра. А именно clip.getBounds(clip). Остальное в образце было сделано неоптимально и бажно. В частности, использовался не getBounds, а getRect, который не учитывает толщину линиий и поэтому обрезает края клипа иногда больше чем на один пиксель. Кроме того, память BitmapData использовалась очень неоптимально, необходимы еще были дополнительные прямоугольники в клипе, сделанные вручную.
Поэтому я сделал свой класс, который имеет следующие преимущества:
1. Вычисляется размер и координаты для каждого кадра анимации. Это позволяет очень сильно экономить память и не гонять по экрану битмапы, которые больше размером чем надо. В частности, если у вас есть анимация прыгающего мячика, или, как у меня, анимация взлетающей ракеты, то часть кадров будут полупустыми, поскольку размер прямоугольника будет захватывать и ракету в верхней части траектории и внизу.

У меня каждый кадр занимает ровно столько, сколько нужно, при этом сохраняются относительные координаты, так что анимация будет проигрываться в точности.

2. Я добавляю один пиксель по краям на всякий случай, поскольку оказалось, что и метод getBounds может не учесть сглаживание, и обрезать полоску в один пиксель. Более того, getBounds, к сожалению, не учитывает фильтры. То-есть, если у вас наложен очень сильный blur или glow — getBounds вернет прямоугольник без учета этого, и может обрезать очень много пикселей. В своих играх я с этим пока не сталкивался. Можно ставить в этих редких случаях почти невидимые точки по краям в клипе, чтобы оконтурить нужный размер. В любом случае, эта проблема пока изящно не решается в любой реализации. Если кто что нароет — напишите.

Код будет внизу поста, а здесь немного по его использованию:
а). Чтобы получить результат, необходимо просто вызвать статический метод
var myRocketAniFrames : BmpFrames = BmpFrames.createBmpFramesFromMC(RocketFlyingAniMovieClip);
б). В полученном BmpFrames в массиве лежат BitmapData кадры. Отдельно лежат координаты для сдвига, чтобы кадры лежали в нужных местах. Например, если вы потом будете пихать кадры в Bitmap, то просто не забудьте присвоить эти координаты его x и y.
var bmp4 : Bitmap = new Bitmap(myRocketAniFrames.frames[4], PixelSnapping.ALWAYS, true);
bmp4.x = myRocketAniFrames.frameXs[4];
bmp4.y = myRocketAniFrames.frameYs[4];
В частности, у себя в классе анимации я имею один Bitmap в котором каждый кадр меняю битмапдату из массива и корректирую координаты.
Код:
package bmpani {
import flash.display.BitmapData;
import flash.display.MovieClip;
import flash.geom.Matrix;
import flash.geom.Point;
import flash.geom.Rectangle;
/**
* ...
* @author Alexander Porechnov
*/
public class BmpFrames {
public var frames : Array;
public var frameXs : Array;
public var frameYs : Array;
public var totalFrames : int;
protected static const INDENT_FOR_FILTER : int = 64;
protected static const INDENT_FOR_FILTER_DOUBLED : int = INDENT_FOR_FILTER * 2;
protected static const DEST_POINT : Point = new Point(0, 0);
public function BmpFrames() {
frames = new Array();
frameXs = new Array();
frameYs = new Array();
totalFrames = 0;
}
public static function createBmpFramesFromMC(clipClass : Class) : BmpFrames {
var clip : MovieClip = MovieClip( new clipClass() );
var res : BmpFrames = new BmpFrames();
var totalFrames : int = clip.totalFrames;
var frames : Array = res.frames;
var frameXs : Array = res.frameXs;
var frameYs : Array = res.frameYs;
var rect : Rectangle;
var flooredX : int;
var flooredY : int;
var mtx : Matrix = new Matrix();
var scratchBitmapData : BitmapData = null;
for (var i : int = 1; i <= totalFrames; i++) {
clip.gotoAndStop(i);
rect = clip.getBounds(clip);
rect.width = Math.ceil(rect.width) + INDENT_FOR_FILTER_DOUBLED;
rect.height = Math.ceil(rect.height) + INDENT_FOR_FILTER_DOUBLED;
flooredX = Math.floor(rect.x) - INDENT_FOR_FILTER;
flooredY = Math.floor(rect.y) - INDENT_FOR_FILTER;
mtx.tx = -flooredX;
mtx.ty = -flooredY;
scratchBitmapData = new BitmapData(rect.width, rect.height, true, 0);
scratchBitmapData.draw(clip, mtx);
var trimBounds : Rectangle = scratchBitmapData.getColorBoundsRect(0xFF000000, 0x00000000, false);
trimBounds.x -= 1;
trimBounds.y -= 1;
trimBounds.width += 2;
trimBounds.height += 2;
var bmpData : BitmapData = new BitmapData(trimBounds.width, trimBounds.height, true, 0);
bmpData.copyPixels(scratchBitmapData, trimBounds, DEST_POINT);
flooredX += trimBounds.x;
flooredY += trimBounds.y;
frames.push(bmpData);
frameXs.push(flooredX);
frameYs.push(flooredY);
scratchBitmapData.dispose();
}
res.totalFrames = res.frames.length;
return res;
}
}
}
- +40
- scmorr
Комментарии (22)
Сканировать и хранить лейблы очень просто, я думаю любой добавит эту функциональность легко.
Все. Дальше уже пользуйтесь как хотите. Можете написать несколько утилити методов, которые будут искать лебл по номеру фрейма, номер фрейма по имени лейбла. Можете заниматься этим уже в классе анимации в методе goto
Кстате класс проигрывания анимаций нужен при использовании замедлений-ускорений в игре, так? Ну тоесть он конечно полезен и в других случаях. Но этот наиболее оправдан.
Я конечно использую BmpFrames для растеризации мувиклипов с одним кадром, например всяких значков. Но для фона миссии стоит просто выделить битмапу размером ровно сколько надо, например 720x480 и прорисовать фон туда. Ибо зачем тебе все танцы и пляски, которые делает BmpFrames в этом случае?
bmpData.copyPixels(scratchBitmapData, trimBounds, DEST_POINT);