Растеризация 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.

Код будет внизу поста, а здесь немного по его использованию:

а). Чтобы получить результат, необходимо просто вызвать статический метод
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

Комментарии (22)

+1
Очень актуальная тема. Как-раз сейчас разбираюсь в рендеринге вектора в битмап.
0
Спасибо, а как быть с лейблами? Кто как убивает анимации? Я пишу в нужном месте лейбл die, можно конечно проверять на тотал фрэймс. Кто как делает? Поделитесь инфой)
0
Я просто ни разу не столкнулся с необходимостью лейблы сканировать. У меня все классы обрастаю именно с возникновением потребностей. Например в Топ Дефенс мне не нужно было ни ускоряться ни замедляться. А в новой игре — надо. Поэтому тут-же проапгрейдил анимационный класс. Аналогично, в Топ Дифенс маск-слои так явно память не выжирали, и только для новой игры я добавил дополнительную обрезку BmpFrames.
Сканировать и хранить лейблы очень просто, я думаю любой добавит эту функциональность легко.
import flash.display.FrameLabel;
 
var labels:Array = clip.currentLabels;

Все. Дальше уже пользуйтесь как хотите. Можете написать несколько утилити методов, которые будут искать лебл по номеру фрейма, номер фрейма по имени лейбла. Можете заниматься этим уже в классе анимации в методе goto
0
Спасибо:)
0
Ну и вообще убивать анимации по окончании — это ответственность не класса BmpFrames. Это ответственно класса анимации, скажем BmpAni, который реализует все методы как в MovieClip — play() stop() gotoAndPlay() gotoAndStop() итд. В частности у меня еще есть флажок — repeat. Если он стоит — анимация ходит по кругу. Если не стоит — то дойдет до конца и остановится. Еще и в конце вызовет любой метод если его передашь в начале.
0
Ну это всё понятно. У меня «структура» кода совсем простая в этом плане. Конечно вс зависит от конечных целей, и я это понимаю. На данный момент я не передаю методы анимации и даже нет общего класса проигрывания. Анимация ходит по кругу если я её не убиваю и не стопаю, сама:)

Кстате класс проигрывания анимаций нужен при использовании замедлений-ускорений в игре, так? Ну тоесть он конечно полезен и в других случаях. Но этот наиболее оправдан.
0
побольше бы таких постов. Плюсую, спасибо:)
0
спасибо!
0
Неистово плюсую, недавно начал писать собственный подобный класс.
0
Вы не поверите, но для меня эта тема сейчас тоже очень актуальна! Огромное спасибо!
0
Это прямо какое-то пересечение мыслей в эгрегоре. Столько многих в эту тему одновременно потянуло :)
0
Спасибо, очень интересная реализация.
0
т. е. этот класс использовать только для анимаций или фоны тоже надо переводить в растр?
0
название поста кагбэ намекает
0
Сам не сталкивался, но известный Хитри в своем блоге утверждал, что cacheAsBitmap у мувиклипа подглючивает. То-есть иногда флеш решает перерендерить мувиклип даже несмотря на то, что ты ничего в нем не менял и не двигал. А это — лаг. Поэтому Хитри советовал растеризовывать фоны, поскольку они часто реально сложные могут быть и тормознут игру нехило. Поэтому фон хорошо растеризовать полюбому. Ну и сколько не встречал — все делают.
Я конечно использую BmpFrames для растеризации мувиклипов с одним кадром, например всяких значков. Но для фона миссии стоит просто выделить битмапу размером ровно сколько надо, например 720x480 и прорисовать фон туда. Ибо зачем тебе все танцы и пляски, которые делает BmpFrames в этом случае?
0
Ну и еще раз — в мувиклипе не только анимация может быть. Например — набор разных по виду кратеров. Потом в игре делаешь каждый раз рандом и после взрыва рисуешь разные кратеры.
0
Перечитал весь пост и несколько комментариев снова и вдумчиво, и стало все понятно. Спасибо.
0
Спасибо за код, позаимствую если не против)
0
Огромное спасибо!
0
Попользовавшись кодом, нашел в нем минус. Он не очень хорошо отрабатывает если внутри анимации есть несколько вложенных анимаций. Они просто не проигрываются и не кэшируются. Выкручиваюсь захватывая кадры в EnterFrame прямо во время воспроизведения т.к. полагаю, что вложенные клипы не идут из за gotoandstop
0
хотя я тупанул. ))) не стоит по ночам в этом ковыряться. это проблема скорее из за copypixel, с draw всё ок по моему. Буду разбираться по утру, а если кто-то опдскажет в чем проблема, буду благодарен.
0
А в этом месте нужно делать lock и unlock?
bmpData.copyPixels(scratchBitmapData, trimBounds, DEST_POINT);
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.