Класс для динамической генерации космоса

Не так давно комрад romixx опубликовал пост, в котором описывал способ создания космоса с помощью Photoshop,FlashIDE и AC3. Все мы ленивы в той или иной степени и я подумал: а как можно сделать такой или почти такой космос только программным кодом?


Во-первых, определим, что нужно получить визуально – это динамически генерируемые звезды и динамически генерируемые туманности. Во-вторых, все должно работать шустро и не тормозить.

Со звездами все просто и объяснять нечего. С туманностями немного сложнее. В АС3 классе BitmapData есть метод perlinNoise. Я думаю, что большинство из вас знает о нем и использовало в своих проектах. Для тех, кто не знаком с шумом Перлина, не вдаваясь в подробности скажу, что данный алгоритм шума назван в честь Кена Перлина, который создал его, работая над компьютерной графикой для фильма Tron в 1982 году и с помощью этого метода можно генерировать текстуры различных природных явлений, ландшафтов и т.д. (облака, вода, дерево и т.д.).Для быстродействия будем выводить всю красоту в bitmap.

Экспериментируя с шумом Перлина я столкнулся с тем, что он очень ресурсоемок. При большом количестве октав и большом размере генерируемой текстуры фпс падал до 4-5 кадров. Поэтом я ограничил число октав до 4 и размер генерируемой текстуры до 180х120 пикселей. В дальнейшем, при отрисовке ее в битмапу, она скейлится до нужного размера. При размере флешки 720х480, т.е. скейл в 4 раза, растр в глаза не бросается.

Звезды я решил рисовать не с помощью шейпа, а рисовать в bitmapdata с помощью setPixel32. Данный метод был выбран для реализации альфа канала у пикселей, т.е. симуляции различного расстояния от звезд до камеры. Вообще можно рисовать звезды и с помощью шейпа, но мне так больше понравилось, да и производительность так чуть повыше.

Таким образом, у меня получилась динамически генерируемая статическая картинка «глубокого космоса». Теперь попробуем полетать по космосу.

Объяснять, как двигать звезды, я думаю, смысла нет. Для имитации движения мимо туманностей воспользуемся параметром offsets в методе perlinNoise. Этот параметр представляет из себя массив точек (Point), которые задают для каждой октавы величину смещения по осям. Можно смещать октавы по разному и тогда получаются интересные эффекты, но мне это не очень понравилось, поэтому будем смещать все октавы одинаково.

Результат можно посмотреть тут. Во флешке курсорные клавиши задают направление движения, любая клавиша стопит движение.

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

Минусы следующие: во-первых, по методу romixx можно сделать космос более красивым, если как следует потрудиться, по моему методу получается либо слишком много туманностей и мало «пустого» черного космоса, либо слишком мало туманностей; во-вторых, он требователен к вычислительной мощности. При увеличении числа октав и размера текстуры катастрофически падает фпс.

Плюсы: во-первых, космос получается динамически, он всегда разный, при условии, что параметр randomSeed метода perlinNoise задан случайным числом; во-вторых, размер флешки получается меньше; в-третьих, конечный результат легко изменить поиграв с параметрами colorTransform, при отрисовке текстуры туманностей, количеством цветовых каналов в функции шума Перлина, количеством октав и другими параметрами этой функции.

Как можно доработать данный метод? Во-первых, сделать размер звезд разным в зависимости от удаления от камеры. Во-вторых, можно совместить этот метод и метод romixx и сгенерированный шум блендить с заблюреной маской, причем маску также можно генерировать случайным образом.

А вот и сам класс. Возможно, что-то не оптимизировано и т.д. Буду рад любым замечаниям и предложениям. В дальнейшем попробую поэкспериментировать с сиплекс шумом все того же Кена Перлина.

package {
        import flash.display.Bitmap;
        import flash.display.BitmapData;
        import flash.display.BitmapDataChannel;
        import flash.display.Sprite;
        import flash.events.Event;
        import flash.events.KeyboardEvent;
        import flash.geom.ColorTransform;
        import flash.geom.Matrix;
        import flash.geom.Point;
        import flash.geom.Rectangle;
        import flash.system.System;
        import flash.ui.Keyboard;
        import flash.utils.getTimer;
        import RND;
        
        /**
         * ...
         * @author plasma_trip
         */
        public class DeepSpace extends Sprite
        {
                private const noiseW:int = 180;
                private const noiseH:int = 120;
                private const dsWidth:int = 720;
                private const dsHeight:int = 480;
                private const starCount:int = 1000;
                
                private var noiseContainer:Bitmap;
                private var starsContainer:Bitmap;
                private var stars:BitmapData;
                private var noise:BitmapData;
                private var colorTransform:ColorTransform;
                private var channels:uint;
                private var m:Matrix;
                private var offset:int;
                private var point:Point;
                private var seed:int;
                private var particles:Vector.<Particle>;
                private var arr:Array;
                private var starsRect:Rectangle;
                private var noiseRect:Rectangle;
                private var destPoint:Point;
                
                //переменные экрана системной инфы, в реальном проекте это надо убрать
                private var scrSystemInfo:ScrSystemInfo;
                private var startTime:Number;
                private var timeTaken:Number;
                private var lastTime:Number;
                
                private var moveLeft:Boolean = false;
                private var moveRight:Boolean = false;
                private var moveUp:Boolean = false;
                private var moveDown:Boolean = false;
                
                public function DeepSpace():void
                {
                        if (stage) init();
                        else addEventListener(Event.ADDED_TO_STAGE, init, false, 0, true);
                }
                
                private function init(e:Event = null):void
                {
                        removeEventListener(Event.ADDED_TO_STAGE, init);
                        
                        //экран для системной инфы, в реальном проекте это надо убрать
                        scrSystemInfo = new ScrSystemInfo();
                        scrSystemInfo.x = 0;
                        scrSystemInfo.y = 400;
                        
                        noiseRect = new Rectangle(0, 0, noiseW, noiseH);
                        starsRect = new Rectangle(0, 0, dsWidth, dsHeight);
                        destPoint = new Point(0, 0);
                        //колорТрансформ для изменения цвета шума
                        colorTransform = new ColorTransform(0.5, 0.4, 0.5, 1, -48, -48, -48, 0);
                        //какие каналы будут участвовать в формировании шума
                        channels = BitmapDataChannel.RED | BitmapDataChannel.BLUE | BitmapDataChannel.GREEN | BitmapDataChannel.ALPHA;
                        //матрица для скейла шума до необходимого размера
                        m = new Matrix(dsWidth / noiseW, 0, 0, dsHeight / noiseH);
                        point = new Point(0, 0);
                        //массив точек смешения октав
                        arr = [point, point, point, point];
                        RND.seed = 0;
                        //случайное начальное значение, используемое для создания случайных числел функцией шума Перлина
                        seed = RND.getInt(1, 100);
                        offset = 1;

                        //создаем контейнеры в которые будем выводить всю прелесть и битмапДаты для звезд и шума
                        noiseContainer = new Bitmap(new BitmapData(dsWidth, dsHeight, false, 0xFF000000));
                        noiseContainer.transform.matrix = m;
                        starsContainer = new Bitmap(new BitmapData(dsWidth, dsHeight, true, 0x00000000));
                        stars = new BitmapData(dsWidth, dsHeight, true, 0x00000000);
                        noise = new BitmapData(noiseW, noiseH, false, 0xFF000000);
                        
                        particles = new Vector.<Particle>(starCount, true);
                        var i:int = starCount;
                        while ( --i > -1)
                        {
                                particles[i] = new Particle();
                                particles[i].setParticle(RND.getInt(0, dsWidth), RND.getInt(0, dsHeight));
                        }
                        
                        addChild(noiseContainer);
                        addChild(starsContainer);
                        //добавляем экран для системной инфы, в реальном проекте его надо убрать
                        addChild(scrSystemInfo);
                        addEventListener(Event.ENTER_FRAME, enterframeHandler);
                        stage.addEventListener(KeyboardEvent.KEY_DOWN, keydownHandler);
                }
                
                private function keydownHandler(e:KeyboardEvent):void
                {
                        moveLeft = moveRight = moveUp = moveDown = false;
                        if (e.keyCode == Keyboard.LEFT) moveLeft = true;
                        if (e.keyCode == Keyboard.RIGHT) moveRight = true;
                        if (e.keyCode == Keyboard.UP) moveUp = true;
                        if (e.keyCode == Keyboard.DOWN) moveDown = true;
                }

                private function enterframeHandler(e:Event):void
                {
                        startTime = getTimer();
                        timeTaken = startTime - lastTime;
                        scrSystemInfo.txtFPS.text = Math.round(1000 / timeTaken).toString();
                        scrSystemInfo.txtRAM.text = (Math.round(System.totalMemory / 1024)) + " kb";
                        scrSystemInfo.txtTime.text = timeTaken.toString();
                        lastTime = startTime;
                        
                        //лочим контейнеры, что бы не перерисовывался флешем во время изменений
                        noiseContainer.bitmapData.lock();
                        starsContainer.bitmapData.lock();
                        //изменяем пойнты в зависимости от направления движения
                        if (moveRight) point.x += offset;
                        if (moveLeft) point.x -= offset;
                        if (moveUp) point.y -= offset;
                        if (moveDown) point.y += offset
                        //собственно генерируем сам шум
                        noise.perlinNoise(noiseW, noiseH, 4, seed, false, true, channels, false, arr);
                        noise.colorTransform(noiseRect, colorTransform);
                        //очишаем битмапДату звезд
                        stars.fillRect(starsRect, 0);
                        //рисуем звезды
                        var i:int = starCount;
                        while ( --i > -1)
                        {
                                var particle:Particle = particles[i];
                                if (moveRight) particle.x -= particle.v;
                                if (moveLeft) particle.x += particle.v;
                                if (moveUp) particle.y += particle.v;
                                if (moveDown) particle.y -= particle.v;
                                stars.setPixel32(particle.x, particle.y, particle.color)
                                if (particle.x < 0) particle.setParticle(dsWidth, RND.getInt(0, dsHeight));
                                if (particle.x > dsWidth) particle.setParticle(0, RND.getInt(0, dsHeight));
                                if (particle.y < 0) particle.setParticle(RND.getInt(0, dsWidth), dsHeight);
                                if (particle.y > dsHeight) particle.setParticle(RND.getInt(0, dsWidth), 0);
                        }
                        //рисуем в контейнер шум и звезды
                        noiseContainer.bitmapData.copyPixels(noise, starsRect, destPoint);
                        starsContainer.bitmapData.copyPixels(stars, starsRect, destPoint);
                        //разлочим контейнеры
                        noiseContainer.bitmapData.unlock();
                        starsContainer.bitmapData.unlock();
                }
        }
        
}

class Particle
{
        public var x:int = 0;
        public var y:int = 0;
        public var v:int = 0;
        public var color:uint;
        
        public function setParticle(setX:int, setY:int):void
        {
                x = setX;
                y = setY;
                v = RND.getInt(1, 10);
                color = (51 * v - 255) << 24 | 0xFF << 16 | 0xFF << 8 | 0xFF;
        }
}


З.Ы. Для генерации случайных чисел использован замечательный класс представленный elmortem. Спасибо ему за это. И спасибо romixx, что натолкнул своим постом на этот эксперимент.

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

0
Ух ты красота!!! Спасибо, я как раз собирался в скором времени космо-игру делать, очень пригодится.
+1
Спасибо! Только нужно попробовать на медленых машинах. На моем ноуте Core Duo T2400 1,83 ГГц 1GbRAM выдает 20фпс. Для ускорения необходимо уменьшать размер генерируемой текстуры и увеличивать скейл, но там возможно полезет растр. Это надо на живом проекте смотреть.
+1
На моем, уже слабеньком, Core 2 Duo T5450 1,6 ГГц 2GbRAM, держит стабильно > 50 fps.
0
+8
Уже третья картинка не в тему. Жаль, что администратор FGB против минусов.
+7
Так это же подделка на zarkua =) скоро сама сломается.
+1
Насколько я знаю, он против минусов к постам, а не к комментам. Надо просто спросить.
+1
Шикарно.
+1
Спасибо. Рад, что понравилось.
+1
Обалденно красивый результат! Плюсую!
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.