15 ноября 2010 г.

Серия уроков по Alternativa3D. Урок I. Приступая к работе

Скачать, Flash-плеер для просмотра

После конференции Adobe очень заинтересовался отечественной разработкой в области 3-мерной графики для Flash движком Alternativa3D. Помимо русскоязычных ресурсов, нашел на просторах интернета обучающий курс от Мэтью Касперсена и решил сделать доброе дело для тех, у кого с английским не очень, — перевести эти уроки. Не уверен, что сил хватит на все уроки, но начало положено.

За перевод судить строго меня не следует — он достаточно вольный в литературном плане. Техническую сторону я старался сохранить максимально достоверной.

Оригинал статьи находится здесь.

Для работы вам понадобится например FlashDevelop, а так же Adobe Flex SDK, который, к слову, подгружается при установке последней версии FlashDevelop. А как настроить FlashDevelop можно прочитать здесь. Также необходимо скачать SWC-библиотеки Alternativa3D и подключать к каждому вашему проекту.

Подключается библиотека swc тривиально. В среде FlashDevelop жмите правую кнопку мыши в окне Project на той папке, в которой хотите разместить библиотеку (например libs) и выбирайте Add->Library Asset... Затем выбирайте файл *.swc и всё, библиотека подключена.

Итак, перевод.

Alternativa — 3D движок для платформы Flash, который позволяет размещать трехмерные объекты прямо на ваших веб-страницах. Учитывая, что Flash player установлен примерно но 90% всех подключенных к Интернету устройств, Alternativa предоставляет вам 3D платформу, которая не требует никаких усилий по установке от конечных пользователей. В этом уроке я покажу вам, как создавать простые 3D-приложения с использованием Альтернатива и Flex.

Хотя конечный результат этого урока прост, тем не менее это не приложение «hello world». С самого начала мы сосредоточимся на том, чтобы создать прочную основу, которую впоследствии можно будет легко расширить. С этой целью один из принципов, который я применю здесь — разделение логики, связанной с управлением 3D движком (т.е. почти весь код, созданный Альтернативой), и логики, которая определяет конечный результате самого приложения.

// EngineManager.as
package
{
    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    import flash.events.Event;
    import mx.collections.ArrayCollection;
    import mx.core.Application;
    import mx.core.UIComponent;
    import alternativa.engine3d.controllers.CameraController;
    import alternativa.engine3d.core.Camera3D;
    import alternativa.engine3d.core.Object3D;
    import alternativa.engine3d.core.Scene3D;
    import alternativa.engine3d.display.View;
    import alternativa.utils.FPS;

    /**
     *     The EngineManager holds all of the code related to maintaining the Alternativa 3D engine.
     */
    public class EngineManager extends UIComponent
    {
        public var scene:Scene3D;
        public var view:View;
        public var camera:Camera3D;
        public var cameraController:CameraController;

        // a collection of the BaseObjects
        protected var baseObjects:ArrayCollection = new ArrayCollection();
        // a collection where new BaseObjects are placed, to avoid adding items
        // to baseObjects while in the baseObjects collection while it is in a loop
        protected var newBaseObjects:ArrayCollection = new ArrayCollection();
        // a collection where removed BaseObjects are placed, to avoid removing items
        // to baseObjects while in the baseObjects collection while it is in a loop
        protected var removedBaseObjects:ArrayCollection = new ArrayCollection();
        // the last frame time
        protected var lastFrame:Date;

        public function EngineManager()
        {
            super();
            addEventListener(Event.ADDED_TO_STAGE, init);
        }

        public function init(e:Event): void
        {
            stage.scaleMode = StageScaleMode.NO_SCALE;
            stage.align = StageAlign.TOP_LEFT;

            // Creating scene
            scene = new Scene3D();
            scene.root = new Object3D();

            // Adding camera and view
            camera = new Camera3D();
            camera.x = 100;
            camera.y = -150;
            camera.z = 100;
            scene.root.addChild(camera);

            view = new View();
            addChild(view);
            view.camera = camera;

            // Connecting camera controller
            cameraController = new CameraController(stage);
            cameraController.camera = camera;
            cameraController.setDefaultBindings();
            cameraController.checkCollisions = true;
            cameraController.collisionRadius = 20;
            cameraController.controlsEnabled = true;

            // FPS display launch
            FPS.init(stage);

            stage.addEventListener(Event.RESIZE, onResize);
            stage.addEventListener(Event.ENTER_FRAME, onEnterFrame);
            onResize(null);

            // set the initial frame time
            lastFrame = new Date();

            // start the application
            ApplicationManager.Instance.startupApplicationManager();
        }

        private function onResize(e:Event):void
        {
            view.width = stage.stageWidth;
            view.height = stage.stageHeight;
            Application.application.width = stage.stageWidth;
            Application.application.height = stage.stageHeight;
        }

        protected function onEnterFrame(event:Event):void
        {
            // Calculate the time since the last frame
            var thisFrame:Date = new Date();
            var seconds:Number = (thisFrame.getTime() - lastFrame.getTime())/1000.0;
            lastFrame = thisFrame;

            // sync the baseObjects collection with any BaseObjects created or removed during the
            // render loop
            removeDeletedBaseObjects();
            insertNewBaseObjects();

            // allow each BaseObject to update itself
            for each (var baseObject:BaseObject in baseObjects)
                baseObject.enterFrame(seconds);

            // User input processing
            cameraController.processInput();
            // Scene calculating
            scene.calculate();
        }

        public function addBaseObject(baseObject:BaseObject):void
        {
            newBaseObjects.addItem(baseObject);
        }

        public function removeBaseObject(baseObject:BaseObject):void
        {
            removedBaseObjects.addItem(baseObject);
        }

        protected function shutdownAll():void
        {
            // don't dispose objects twice
            for each (var baseObject:BaseObject in baseObjects)
            {
                var found:Boolean = false;
                for each (var removedObject:BaseObject in removedBaseObjects)
                {
                    if (removedObject == baseObject)
                    {
                        found = true;
                        break;
                    }
                }

                if (!found)
                    baseObject.shutdown();
            }
        }

        protected function insertNewBaseObjects():void
        {
            for each (var baseObject:BaseObject in newBaseObjects)
                baseObjects.addItem(baseObject);

            newBaseObjects.removeAll();
        }

        protected function removeDeletedBaseObjects():void
        {
            for each (var removedObject:BaseObject in removedBaseObjects)
            {
                var i:int = 0;
                for (i = 0; i < baseObjects.length; ++i)
                {
                    if (baseObjects.getItemAt(i) == removedObject)
                    {
                        baseObjects.removeItemAt(i);
                        break;
                    }
                }

            }

            removedBaseObjects.removeAll();
        }
    }
}

Мы начнем с класса EngineManager. Этот класс будет содержать весь код, необходимый для инициализации и запуска движка Alternativa3D (далее A3D, моё прим.). Класс EngineManager расширяет класс UIComponent, который мы раместим в корне Flex-приложения как любой другой GUI-контрол. Мы увидим это позже в файле Alternativa1.mxml.

В конструктор EngineManager мы добавили приемник событий, который будет вызывать функцию инициализации каждый раз, когда EngineManager будет добавлен в список отображения. Если фраза "добавить к списку отображения" ничего не значит для вас, не волнуйтесь — все, что вам нужно знать, это то, что когда произошло это событие объект stage больше не NULL.

Функция инициализации выполняет большинство кода инициализации A3D. Мы создаем 4 важнейших объекта A3D: Scene3D, Camera3D, CameraController и View. Scene3D, по существу, контейнер в котором размещаются все наши объекты сцены. Camera3D, как следует из названия, точка обзора для нашего 3D-мира. CameraController — удобный класс, который позволяет нам двигать камеру клавиатурой и мышью, а также определяет столкновения с объектами сцены. CameraController дает Вам возможность передвигаться и интерактивно взаимодействовать с 3D-миром написав всего 6 строк кода. Это круто! Наконец, есть объект View, принимающий наше 3D-окружение, который видит камера и переводит его в 2D-изображение, которое будет отображаться на мониторе.

Отметим также метод, названный FPS.init(stage), который размещает счетчик FPS (кадров в секунду) на экране. Это полезно для мониторинга вашего приложения, но его нужно закомментировать при окончательном развертывании вашего приложения.

После инициализации движка A3D мы добавим еще два слушателя событий: один — для реакции на изменение размеров окна ( stage.addEventListener(Event.RESIZE, OnResize):void ), второй — для реакции на событие перерисовки кадра ( stage.addEventListener (Event. ENTER_FRAME, onEnterFrame):void ). Мы перехватываем изменение размеров окна с целью обновления размеров нашего View. Событие же перерисовки кадра — наш основной цикл растеризации кадра (далее "основной цикл", моё прим.).

Основной цикл — общий термин, который обозначает цикл, определяющий работу вашего приложения. Цикл состоит из двух частей. Первая — когда приложение обновляется. Любое движение или логика вашей игры просчитывается в первой чаксти цикла, скажем, расчет перемещения ракеты в пространстве. Вторая — где 3D-движок растеризует кадр на экране, демонстрируя тем самым изменения, которые были сделаны в 3D-мире.

Обновление приложения (первая часть цикла) осуществляется в классе BaseObject. Отметим, что функция onEnterFrame нашего приложения вызывает функцию enterFrame коллекции BaseObject-ов. Единственная цель функции enterFrame класса BaseObject — позволить любому классу, расширяющему BaseObject легко обновлять себя внутри основного цикла. Мы увидим как это работает, чуть позже в классах MeshObject и RotatingBox.

Наконец, в функции инициализации мы вызываем ApplicationManager.Instance.startupApplicationManager(). Я говорил, что мы будем отделять логику приложения от логики движка. Класс EngineManager заботится об управлении движком A3D и об основном цикле, чтобы наша программа на самом деле что-то делала. Для этого мы создаем класс ApplicationManager.

// ApplicationManager.as
package
{

    import mx.core.Application;

    /**
     *     The ApplicationManager holds all program related logic.
     */
    public class ApplicationManager
    {
        protected static var instance:ApplicationManager = null;

        public static function get Instance():ApplicationManager
        {
            if (instance == null)
                instance = new ApplicationManager();
            return instance;
        }

        public function ApplicationManager()
        {

        }

        public function startupApplicationManager():ApplicationManager
        {
            var rotatingBox:RotatingBox = new RotatingBox().startupRotatingBox();
            Application.application.engineManager.cameraController.lookAt(rotatingBox.model.coords);

            return this;
        }

    }
}

ApplicationManager разработан как Singleton. И хотя ActionScript не имеет private и protected конструкторов (и, следовательно, не позволяет реализовывать синглтоны по-настоящему), что имеют некоторые негативные последствия, я по-прежнему нахожу этот шаблон проектирования полезным как инструмент для самодокументирования. Если класс имеет свойство Instance, то он — синглтон, и вы не должны создавать объекты с помощью new.

ApplicationManager в этом примере имеет только одну функцию (помимо Синглтон-свойств): startupApplicationManager. В этой функции мы размещаем код, который относится к самому приложению, в отличие от кода инициализации движка A3D. В нашем случае мы создаем RotatingBox и направляем на него камеру. Это может показаться излишним, определять класс из 2 строк кода, но разделение логики приложения и движка будет полезным в более сложных приложениях.

// BaseObject.as
package
{
    import mx.core.Application;

    /**
     *    The BaseObject class allows extending classes to update themselves during the render loop.
     */
    public class BaseObject
    {
        public function BaseObject()
        {

        }

        /**
         *     Must be called by all extending classes when being created. Adds this object to the list of BaseObjects maintained
         *     by the EngineManager.
         */
        public function startupBaseObject():void
        {
            Application.application.engineManager.addBaseObject(this);
        }

        /**
         *     Must be called by all extending classes when being destroyed. Removes this object to the list of BaseObjects maintained
         *     by the EngineManager.
         */
        public function shutdown():void
        {
            Application.application.engineManager.removeBaseObject(this);
        }

        /**
         *     This function is called once per frame before the scene is rendered.
         *
         *     @param dt The time in seconds since the last frame was rendered.
         */
        public function enterFrame(dt:Number):void
        {

        }
    }
}

// MeshObject.as
package
{
    import alternativa.engine3d.core.Object3D;
    import alternativa.engine3d.materials.SurfaceMaterial;

    import mx.core.Application;

    public class MeshObject extends BaseObject
    {
        public var model:Object3D = null;

        public function MeshObject()
        {
            super();
        }

        override public function shutdown():void
        {
            super.shutdown();
            Application.application.engineManager.scene.root.removeChild(model);
            model = null;
        }

        public function startupModelObject(object:Object3D):void
        {
            model = object;
            Application.application.engineManager.scene.root.addChild(model);
            super.startupBaseObject();
        }
    }
}

Класс RotatingBox является примером того, как расширить класс BaseObject, хотя, если вы посмотрите внимательно, мы фактически создали промежуточный класс MeshObject. MeshObject расширяет BaseObject и добавляет свойства класса Object3D, который представляет собой 3D-сетку на экране. Это может показаться излишним — создавать еще один класс ради одного свойства, но правда в том, что вам может понадобиться обновлять объекты сцены вашего приложения или игры каждый кадр и они не обязательно должны иметь свойства 3D-сетки. Именно поэтому мы создали BaseObject и MeshObject как отдельные классы: расширение BaseObject позволяет объекту обновить себя, а MeshObject будет общим базовым классом для тех объектов, которые имеют свойства 3D-сетки, такие как RotatingBox, например.

// RotatingBox.as
package
{
    import alternativa.engine3d.materials.WireMaterial;
    import alternativa.engine3d.primitives.Box;

    public class RotatingBox extends MeshObject
    {
        protected static const ROTATION_SPEED:Number = 1;

        public function RotatingBox()
        {
            super();
        }

        public function startupRotatingBox():RotatingBox
        {
            var box:Box = new Box(100, 100, 100, 3, 3, 3);
            box.cloneMaterialToAllSurfaces(new WireMaterial(1, 0x000000));
            super.startupModelObject(box);
            return this;
        }

        public override function enterFrame(dt:Number):void
        {
            model.rotationX += dt * ROTATION_SPEED;
            model.rotationY += dt * ROTATION_SPEED;
            model.rotationZ += dt * ROTATION_SPEED;
        }
    }
}

Итак, давайте взглянем на класс RotatingBox. У нас есть две важные функции: startupRotatingBox и enterFrame. Функция StartupRotatingBox отвечает за создание и текстурирование 3D-сетки, которая будет отображаться на экране. Мы используем встроенный в движок Box-примитив, и текстуру проволочного каркаса WireMaterial. Мы переопределяем функцию enterFrame класса BaseObject, и именно здесь мы вращаем наш объект изменяя немного угол в каждом кадре.

<!-- Alternativa1.mxml -->
<?xml version="1.0" encoding="utf-8"?>
<mx:Application
    xmlns:mx="http://www.adobe.com/2006/mxml"
    layout="absolute"
    xmlns:ns1="*"
    width="640"
    height="480" color="#FFFFFF"
    backgroundGradientAlphas="[1.0, 1.0]"
    backgroundGradientColors="[#FFFFFF, #C0C0C0]">

    <ns1:EngineManager id="engineManager" x="0" y="0" width="100%" height="100%"/>
    <mx:Image x="10" y="10" id="imgAlternativa" source="@Embed(source='../media/alternativa.jpg')"/>

</mx:Application>

Собираем все вместе в файле Alternativa1.mxml. Это точка входа приложения. Как я упоминал ранее, EngineManager класс расширяет UIComponent класс, что позволяет ему быть добавленым в качестве дочернего в главный класс приложения. Вы можете заметить, что мы добавили EngineManager как дочерний элемент, как Button или Label.

Ну, и что же у нас получилось? Класс EngineManager выполняет код, необходимый для инициализации и управления движком A3D. Этот код меняет очень не много в этом примере. Класс ApplicationManager выполняется код, который относится к логике самого приложения. Вы увидите, что этот код будет меняться почти во всех примерах. Классы BaseObject и MeshObject дают нам простой способ создать объект, который может обновлять себя в основном цикле. Эти классы претерпят значительные изменения в будущих уроках. И, наконец, у нас есть класс RotatingBox, который показывает как расширить класс MeshObejct для создания самообновлеющегося 3D-объекта.

Что получилось можно увидеть здесь, исходный код — здесь

8 комментариев:

Антон Волков комментирует...

Только вот Касперсон писал урок по Alternativa3D 5, а не 7 :)

Иван комментирует...

Ссылка не та... да... исправил. Спасибо.

Аутсорсинг комментирует...

Пермские ребята движок делают)

Иван комментирует...

Вот прям в точку...)))

Аутсорсинг комментирует...

У движка есть серьезный минус - отсутствие освещения и, соответственно, теней =(
Обещают в 8-версии поправить и в разы увеличить производительность =)

Иван комментирует...

Производительность, к тому времени когда выйдет 8 версия Альтернативы, увеличится не только у них. Все с нетерпением ждут Molehill с поддержкой API для прямого обращения к ресурсам GPU. Так что, если всё получится, то нас ждут интереснейшие события в области браузерных технологий и в частности 3D.

А отсутствие освещения и теней не является минусом Альтернативы, т.к. основной упор разработчиками был сделан на производительности, поэтому функционал теней и освещения был не реализован намеренно.

Аутсорсинг комментирует...

Появились тени )))
http://blog.alternativaplatform.com/ru/2011/03/12/alternativa3d-7_7_0-update/

Иван комментирует...

Известно, спасибо, но сейчас более интересное направление для изучения API Molehill. Всем рекомендую!

Отправить комментарий