17 ноября 2010 г.

Серия уроков по Alternativa3D. Урок III. Загрузка модели

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

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

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

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

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

Перевод:

Основная функция любого трехмерного движка — подгрузка 3D-моделей. Библиотека Alternativa3D (5 версии, моё прим.) позволяет загружать и хранить модели в 2-х форматах 3DS и OBJ. Данный урок научит вас как это сделать.

В последнем уроке мы создали класс ResourceManager как место для размещения изображения, которое использовали как текстуру. Это очень удобно — хранить все файлы и ресурсы приложения в одном месте. К сожалению, класс Альтернативы Loader3DS, который мы используем, позволяет загружать и парсить файл 3DS только принимая URL этого файла и не может внедрить файл в наше конечное приложение. Это значит, что файл 3DS будет хранится отдельно от файла SWF и будет подгружаться специально.

Вдобавок ко всему 3DS файл загружается асинхронно, а это означает, что сам процесс загрузки и разбора 3DS файла осуществляется в фоновом режиме. Хотя Flash-приложения могут продолжать выполнение, пока какие-то данные подгружаются асинхронно, все же мы внесем некоторые изменения в классы EngineManager и ResourceManager для того, чтобы начинать выполнение нашего приложения только после того как все необходимые ресурсы будут загружены.

// ResourceManager.as
package
{
    import alternativa.engine3d.core.Mesh;
    import alternativa.engine3d.core.Object3D;
    import alternativa.engine3d.loaders.Loader3DS;
    import alternativa.engine3d.materials.TextureMaterialPrecision;
    import alternativa.utils.MeshUtils;

    import flash.events.Event;
    import flash.events.IOErrorEvent;
    import flash.events.SecurityErrorEvent;

    import mx.core.Application;

    /**
     *     ResourceManager is where we embed and create the resources needed by our application
     */
    public class ResourceManager
    {
        protected static const SERVER_URL:String = "http://alternativatut.sourceforge.net/media/";

        public static var fighter:Mesh = null;
        protected static var fighterLoader:Loader3DS = null;
        protected static var fighterLoaded:Boolean = false;

        public static function get allResourcesLoaded():Boolean
        {
            return fighterLoaded;
        }

        public static function loadResources():void
        {
            fighterLoader = new Loader3DS();
            fighterLoader.smooth = true;
            fighterLoader.precision = TextureMaterialPrecision.HIGH;

            // watch out - case counts on some web servers, but not in windows
            fighterLoader.load(SERVER_URL + "fighter.3DS");
            fighterLoader.addEventListener(Event.COMPLETE, onLoadFighterComplete);
            fighterLoader.addEventListener(IOErrorEvent.IO_ERROR, ioError);
            fighterLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityError);
        }

        protected static function ioError(e:Event):void
        {
            Application.application.lblLoading.text = "Error Loading";
        }

        protected static function securityError(e:Event):void
        {
            Application.application.lblLoading.text = "Error Loading";
        }

        protected static function weldVerticesAndFaces(object:Object3D):void
        {
            if (object != null)
            {
                if (object is Mesh)
                {
                    MeshUtils.autoWeldVertices(Mesh(object), 0.01);
                    MeshUtils.autoWeldFaces(Mesh(object), 0.01, 0.001);
                }

                // Launching procedure for object's children
                for (var key:* in object.children)
                {
                    weldVerticesAndFaces(key);
                }
            }
        }

        protected static function onLoadFighterComplete(e:Event):void
        {
            for (var o:* in fighterLoader.content.children)
            {
                fighter = o;
                weldVerticesAndFaces(fighter);
                break;
            }

            fighterLoaded = true;
        }
    }
}

Давайте взглянем на наш класс ResourceManager. Как вы заметили, асинхронные процессы загрузки 3D-моделей требуют добавления нескольких новых функций. Как обычно, мы объявляем public static свойство для нашего ресурса (названного здесь fighter), которое позволит иметь доступ к модели во всем приложении, когда она будет загружена. Свойство fighterLoader — это экземпляр класса 3DSLoader, который выполняет всю работу по загрузке и разбору 3DS файла. Мы также определяем булево свойство fighterLoaded, которые будем использовать как сигнал для остальной части системы, что 3D-модель была загружена и готова к использованию.

Фунция allResourcesLoaded просто возвращает значение fighterLoaded. Если бы мы загружали несколько 3D-моделей, то эта функция возвращала бы true только при условии, что все эти модели загружены. Однако, так как мы загружаем единственную модель, эта функция возвращает значение одного логического флага.

Далее, у нас есть функция loadResources. В ней мы создаем новый экземпляр класса Loader3DS, инициализируем его и вызываем запрос на загрузку указанного 3DS файла, а затем подключаем несколько функций, реагирующих на события, вызываемые классом Loader3DS. Помните, что я говорил про асинхронную загрузку 3DS-файлов? Следствием этого является то, что после вызова fighterLoader.load(SERVER_URL + "fighter.3DS"), программа продолжает выполняться дальше, не дожидаясь завершения выполнения функции и модель еще не загружена. Модель становится загруженной только при наступлении события Event.COMPLETE, для которого мы регистрируем обработчик fighterLoader.addEventListener(Event.COMPLETE, onLoadFighterComplete):void, а после выполнения запроса на загрузку 3DS-файла программа продолжает весело выполняться далее. К сожалению, так как единственной целью нашего приложения является отображение загруженной 3D-модели, мы не может продолжать выполнение программы дальше, пока она не станет доступна.

Таким образом, функция allResourcesLoaded со своим флагом fighterLoaded становится очень важной. Присмотритесь к функции onLoadFighterComplete. Как только модель загружается и становится доступной для нас мы извлекаем её из коллекции fighterLoader.content.children (мы знаем, что модель в коллекции одна и берем первый элемент, сразу завершая цикл) и устанавливаем флаг fighterLoaded в true. Запомните эти важные изменения в классе EngineManager.

Функции IOError и SecurityError вызываются, если Loader3DS не может загрузить указанный файл 3DS. В этом случае мы просто печатаем ошибку, чтобы дать понять конечному пользователю, что что-то пошло не так.

Функция weldVerticesAndFaces использует два метода доступные через класс Альтернативы MeshUtils: autoWeldVertices и autoWeldFaces. Метод autoWeldVertices просматривает вершины, из которых состоит модель и объединяет их в одну, если они расположены достаточно близко друг от друга. Аналогично autoWeldFaces объединяет любые полигоны, которые достаточно компланарны (т.е. полигоны, которые лежат в очень близких плоскостях) в один полигон. Целью этого является сведение к минимуму числа отдельных полигонов, которые Альтернатива просчитывает в каждом кадре, чтобы повысить производительность.

// EngineManager.as
package
{
    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;

    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    import flash.events.Event;

    import mx.collections.ArrayCollection;
    import mx.core.Application;
    import mx.core.UIComponent;

    /**
     *     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;

        protected var applicationManagerStarted:Boolean = false;

        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();
            scene.splitAnalysis = false;

            // 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 = false;

            // 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();

            // load the resources
            ResourceManager.loadResources();
        }

        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
        {
            if (ResourceManager.allResourcesLoaded)
            {
                if (!applicationManagerStarted)
                {
                    applicationManagerStarted = true;
                    // start the application
                    ApplicationManager.Instance.startupApplicationManager();
                    // remove the loading lable
                    Application.application.lblLoading.visible = false;
                }

                // 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. Первое — в функции инициализации. Последняя строка функции инициализации используется для запуска класса ApplicationManager. Однако теперь эта строка заменена на вызов функции loadResources класса ResourceManager. Вызывая эту функцию мы указываем ResourceManager начать загрузку внешних файлов 3DS.

Второе изменеие касается функции onEnterFrame. Заметим, что содержимое этой функции будет запущено единажды после того, как ResourceManager.allResourcesLoaded вернет true. Результатом этого является то, что основной цикл растеризации не начнет выполняться пока не будут загружены все ресурсы. После загрузки ресурсов следует один вызов функции ApplicationManager.Instance.startupApplicationManager(), запускет наше приложение. Флаг applicationManagerStarted позволяет нам убедиться, что эта функция вызвана единажды. Также мы удаляем компонент Label из списка отображения, который информирует пользователя о загрузке.

// ApplicationManager.as
package
{
    import alternativa.types.Point3D;

    import mx.core.Application;

    /**
     *     The ApplicationManager holds all program related logic.
     */
    public class ApplicationManager extends BaseObject
    {
        protected static var instance:ApplicationManager = null;
        protected static const CAMERA_MOVEMENT_BOX:Number = 100;
        protected static const CAMERA_MOVEMENT_BOX_HALF:Number = CAMERA_MOVEMENT_BOX / 2;
        protected static const CAMERA_SPEED:Number = 25;
        protected var mesh:MeshObject = null;
        protected var target:Point3D= null;

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

        public function ApplicationManager()
        {
            super();
        }

        public function startupApplicationManager():ApplicationManager
        {
            mesh = new MeshObject().startupModelObject(ResourceManager.fighter);
            super.startupBaseObject();
            target = generateRandomTarget();
            return this;
        }

        public override function enterFrame(dt:Number):void
        {
            var direction:Point3D = Point3D.difference(target, Application.application.engineManager.camera.coords);
            var distToMoveThisFrame:Number = dt * CAMERA_SPEED;

            // don't overshoot the target point
            if (Math.pow(distToMoveThisFrame, 2) >= direction.lengthSqr)
            {
                distToMoveThisFrame = direction.length;
                target = generateRandomTarget();
            }

            direction.normalize();
            direction.multiply(dt * CAMERA_SPEED);
            Application.application.engineManager.camera.coords = Point3D.sum(Application.application.engineManager.camera.coords, direction);
            Application.application.engineManager.cameraController.lookAt(mesh.model.coords);
        }

        protected function generateRandomTarget():Point3D
        {
            // get a random target
            return Point3D.random(-CAMERA_MOVEMENT_BOX, CAMERA_MOVEMENT_BOX, -CAMERA_MOVEMENT_BOX, CAMERA_MOVEMENT_BOX, -CAMERA_MOVEMENT_BOX, CAMERA_MOVEMENT_BOX);
        }
    }
}

Ресурсы загружаются и приложение запускается как обычно. Класс ApplicationManager использует ресурсы из ResourceManager ничего не ведая об асинхронной загрузке ресурсов.

Стоит отметить, что мы сделали класс ApplicationManager расширяющим BaseObject-класс. В первом уроке я упомянул, что иногда объект должен обновляться каждый кадр, не имея при этом какой-либо визуализируемой геометрии, именно поэтому мы разделили BaseObject и MeshObject классы. В нашем случае ApplicationManager обновляется каждый кадр, перемещая камеру вокруг случайной точки 3D модели пока камера смотрит на эту модель с помощью функциии lookAt класса CameraController.

При асинхронной загрузке ресурсов вам почти всегда захочется, чтобы модели были доступны перед запуском приложения или просчетом сцены. С изменениями, которые мы сделали в классах EngineManager и ResourceManager вы сможете загружать и использовать ресурсы в других классах, как в ApplicationManager, например, так как если бы они были загружены синхронно.

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

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

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

Будете делать серию своих уроков?

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

Я бы с удовольствием, но, к сожалению, на это пока абсолютно нет времени - работа... :(

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

А жаль у вас здорово получается )

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

Спасибо, периодически-то всё-равно буду что-то писать пока. Просто серию вряд ли буду затевать.

Wetux комментирует...

Вот как раз то, что надо. Ещё бы в pdf формате, чтобы печатать было удобно...

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

Достаточно не сложно выпарсить же... Да и не думаю, что эти уроки еще актуальны. Они по 5 версии, а уже принципиально другая 8-я. Хотя некоторый верхний уровень абстракции конечно сохранился. Могу сделать в пдф, если сильно надо.

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