65.9K
CodeProject 正在变化。 阅读更多。
Home

Away3D 入门

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2009年8月23日

CPOL

9分钟阅读

viewsIcon

27136

downloadIcon

334

了解如何使用 Away3D 为您的 3D Flash 应用程序创建一个简单的框架。

引言

Away3D 是一个强大的 Flash 3D 引擎,它最初是从 Papervision Flash 3D 引擎衍生出来的。自那时以来,它已发展成为一个独立的项目,目前是为 Flash 提供的少数几个可以利用 Flash Player 10 新功能的 3D 引擎之一。

在本教程中,我将向您展示如何启动并运行一个基本的 Away3D 程序。这将是一系列教程的第一篇,因此,我们在这里将打下坚实的基础,以便后续教程可以建立在此基础上。这意味着,虽然最终的成果会很简单,但我们会因此创建几个类。

背景

此代码用作演示 Away3D 引擎的教程系列 的基础。

使用代码

程序本身在概念上将分为两个区域:“引擎”和“应用程序”。这样做的原因是,将有大量的代码用于创建、运行和清理 Away3D“引擎”,而这部分代码在大多数教程中都是通用的。另一方面,“应用程序”在不同的教程中会有显著的变化。通过将构成这两个区域的代码分开,我们可以定义一个可重用的基础,用于所有后续教程,并且它还有助于将实现教程结果的代码与运行 Away3D 引擎的样板代码隔离开来。

我们需要创建的第一个类是 EngineManager。顾名思义,此类将负责创建、运行和销毁 Away3D 引擎。

EngineManager.as
package
{
    import away3d.cameras.Camera3D;
    import away3d.containers.View3D;
    import away3d.core.render.Renderer;
    
    import mx.collections.ArrayCollection;
    import mx.core.UIComponent;
    import mx.core.Application;
    
    import flash.events.*;

    public class EngineManager extends UIComponent
    {
        public static const version:String = "1.0.0";
        protected static const MEASURED_MIN_WIDTH:int = 25;
        protected static const MEASURED_MIN_HEIGHT:int = 25;
        protected static const MEASURED_WIDTH:int = 100;
        protected static const MEASURED_HEIGHT:int = 100;
        
        // Away3D view
        internal var view:View3D = null;
        // Away3D camera
        internal var cam:Camera3D = null;        
        // 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;
        // the application manager
        protected var applicationManager:ApplicationManager = null;
        // the resource manager
        protected var myResourceManager:ResourceManager = null;
        // true when we have added the Away3D controls
        protected var addedToStage:Boolean = false;
        // true if some properties have been modifed
        protected var propertiesDirty:Boolean = false;

        internal function get MyResourceManager():ResourceManager
        {
            return myResourceManager;
        }
        
        protected function propertyChanged():void
        {
            propertiesDirty = true;
            invalidateProperties();
            invalidateDisplayList();
        }
        
        public function EngineManager()
        {
            super();            
        }
        
        override protected function measure():void
        {
            super.measure();
            
            // set a bunch of predefined sizes
            this.measuredMinWidth = MEASURED_MIN_WIDTH;
            this.measuredMinHeight = MEASURED_MIN_HEIGHT;
            this.measuredHeight = MEASURED_HEIGHT;
            this.measuredWidth = MEASURED_WIDTH;
        }
        
        override protected function updateDisplayList(unscaledWidth:Number, 
                           unscaledHeight:Number):void
        {
            super.updateDisplayList(unscaledWidth, unscaledHeight);
            
            if (view != null)
            {
                // resize the viewport to match the new settings
                view.x = this.height/2;
                view.y = this.width/2;
            }
        }
        
        override protected function commitProperties():void
        {
            super.commitProperties();
            
            if (propertiesDirty)
            {
                propertiesDirty = false;
                
                if (addedToStage)
                {
                    applicationManager.shutdown();
                    applicationManager.startupApplicationManager();
                }
            }    
        }
        
        override protected function createChildren():void
        {
            super.createChildren();

            addEventListener(Event.ADDED_TO_STAGE, createChildrenEx);
            addEventListener(Event.REMOVED_FROM_STAGE, shutdown);            
        }
        
        protected function shutdown(event:Event):void
        {
            if (applicationManager != null)
                applicationManager.shutdown();
                        
            shutdownAll();
            
            this.removeChild(view);
            
            applicationManager = null;
            addedToStage = false;
            view = null;
            cam = null;
        }
        
        protected function createChildrenEx(event:Event):void
        {
            if (!addedToStage)
            {
                cam = new Camera3D();
                
                view = new View3D({x:250,y:150,camera:cam});
                view.renderer = Renderer.BASIC;
                addChild(view);
                addEventListener(Event.ENTER_FRAME, onEnterFrame);
                
                // set the initial frame time
                lastFrame = new Date();        
                
                // load any resources
                myResourceManager = new ResourceManager();
                myResourceManager.loadResources();
                
                // start the application manager
                applicationManager = 
                  new ApplicationManager(this).startupApplicationManager();
                
                addedToStage = true;
            }
        }
                
        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);
            
            // render the scene
            view.render();
        }
        
        public function addBaseObject(baseObject:BaseObject):void
        {
            newBaseObjects.addItem(baseObject);
        }
        
        public function removeBaseObject(baseObject:BaseObject):void
        {
            removedBaseObjects.addItem(baseObject);
        }
        
        public 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 涉及的代码量相当大。之所以这样,是因为我们将 EngineManager 创建为一个 Flash 组件(请参阅 Adobe 文档 此处)。通过扩展 UIComponent 类并覆盖一些关键函数,我们可以创建一个类,任何对底层代码一无所知的人都可以将其拖放到 Flash 或 Flex 应用程序中。

EngineManager 的功能之一是允许任何扩展 BaseObject 类(稍后详细介绍)的类在帧渲染到屏幕之前更新自身。为此,我们需要维护一个 BaseObject 的集合,这就是 baseObjectsnewBaseObjectsremovedBaseObjects 属性的由来。baseObjects 属性保存所有当前活动的 BaseObject,而 newBaseObjectsremovedBaseObjects 包含刚刚添加到系统或从中移除的 BaseObject

我们不直接在 baseObjects 中添加和移除对象的原因是,在遍历集合时修改它几乎总是个坏主意。

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

看看我们如何遍历 baseObjects 集合并对每个 BaseObject 调用 enterFrame。如果我们直接在 baseObjects 中添加或移除一个 BaseObject 类的实例,并且在调用 enterFrame 时创建了一个新的 BaseObject 或将其自身从系统中移除,那么我们就会发现集合不一致。您可能会发现 `foreach` 循环跳过了一个记录,或者访问了一个记录两次。事实上,许多编程语言明确禁止在循环期间修改集合,否则会抛出异常,即使在不允许这样做的语言中,也最好避免这样做。

一旦您理解了维护新添加和已移除 BaseObject 的单独集合的原因,您就可以解释 EngineManager 类中大部分代码的含义。addBaseObjectremoveBaseObject 函数分别将 BaseObject 添加到 newBaseObjectsremovedBaseObjects 集合中,而 insertNewBaseObjectsremoveDeletedBaseObjects 函数(从 onEnterFrame 函数调用)用于将主 baseObjects 集合与已添加和已移除的 BaseObject 同步。

EngineManager 包含六个在 Flash/Flex 组件生命周期中使用的函数。其中四个在 Adobe 文档中有详细介绍,我在这里将一一介绍。

measure 函数用于定义控件的最小和默认大小。这非常直接,因为我们只需要为底层 UIComponentmeasuredMinWidthmeasuredMinHeightmeasuredHeightmeasuredWidth 属性赋值即可。

updateDisplayList 函数用于调整控件子项的大小和位置。在这种情况下,我们唯一的子元素是 Away3D 引擎,具体来说是 View3D 对象。在这里,我们只是调整 `view` 属性的大小,以反映 EngineManager 控件大小的变化。

commitProperties 函数用于允许控件应用任何已做的属性更改。其理念是,属性可以而且会按任何顺序更改,但可能需要按特定顺序应用或处理。尽管 EngineManager 没有公开任何可更改的属性,但这里的代码设置用于重新初始化 ApplicationManager(稍后将详细介绍该类),这实际上会重启应用程序。

createChildren 函数在控件预期创建任何子项时被调用。如前所述,EngineManager 控件的唯一子项是 Away3D 引擎;但是,我们此时并不创建引擎。Away3D 引擎会多次引用 `stage` 属性,而该属性在 `ADDED_TO_STAGE` 事件触发之前为 `null`。因此,我们将 `createChildrenEx` 函数附加到此事件,然后在事件触发时创建 Away3D 引擎。同样,我们使用 `REMOVED_FROM_STAGE` 来调用 `shutdown`,它将清理 Away3D 引擎。

updateDisplayListcommitPropertiescreateChildrenmeasure 函数都具有由 UIComponent 类定义的函数。还有另外两个函数,shutdowncreateChildrenEx,它们也起着重要作用。

如上所述,createChildrenEx 函数是 Away3D 引擎实际启动的地方。对于这个简单的示例,我们只需要两个 Away3D 类:Camera3DView3DCamera3D 类(我们将其分配给 `cam` 属性)是我们观察 Away3D 世界的相机。View3D 类(我们将其分配给 `view` 属性)负责将 3D 世界渲染到您的 2D 显示器上。初始化 Away3D 引擎的实际代码不超过几行。我们创建一个 Camera3D 类的新实例,然后创建一个 View3D 类的新实例。然后我们定义我们想要的渲染器类型(在本例中是基本的渲染器),并将 View3D 添加为 EngineManager 控件的子元素。

除了初始化 Away3D 引擎外,createChildrenEx 函数还负责创建新的 ResourceManagerApplicationManager,并将 `onEnterFrame` 函数附加到 `ENTER_FRAME` 事件。

shutdown 函数用于清理 Away3D 引擎。“清理”本质上是指反向运行 `createChildrenEx` 中的代码,移除已添加的子项,并将已初始化的属性设置为 `null`。

最后,在 `onEnterFrame` 函数中,我们管理我们的渲染循环。在这里,我们确定自上一帧渲染以来经过的时间,同步 baseObjects 集合,对我们所有的 BaseObject 调用 `enterFrame`,然后最后通过 `view.render()` 将帧渲染到屏幕。

BaseObject.as
package
{
    internal class BaseObject
    {
        protected var engineManager:EngineManager = null;
        
        public function BaseObject(engineManager:EngineManager)
        {
            this.engineManager = engineManager;
        }
        
        public function startupBaseObject():void
        {
            engineManager.addBaseObject(this);
        }
        
        public function shutdown():void
        {
            engineManager.removeBaseObject(this);
        } 
        
        public function enterFrame(dt:Number):void
        {
            
        }
    }
}

BaseObject 类的唯一目的是允许扩展类在调用 `enterFrame` 时更新自身。startupBaseObjectshutdown 函数用于将 BaseObject 添加到 EngineManagers 集合中并从中移除。然后,我们有一个空的 `enterFrame` 函数。`enterFrame` 函数预计会被扩展类覆盖。

MeshObject.as
package
{
    import away3d.core.base.Object3D;
    import away3d.core.utils.Init;
    import away3d.loaders.Collada;
    
    import mx.controls.Alert;
    
    import flash.events.Event;

    internal class MeshObject extends BaseObject
    {
        public var model:Object3D = null;
        
        public function MeshObject(engineManager:EngineManager)
        {
            super(engineManager);
        }
        
        public function startupColladaModelObject(collada:XML, init:Init):MeshObject
        {
            super.startupBaseObject();
            if (collada != null)
                model = Collada.parse(collada, init);
            engineManager.view.scene.addChild(model);
            return this;
        }
        
        public override function shutdown():void
        {
            super.shutdown();
            if (model != null)
                engineManager.view.scene.removeChild(model);
            model = null;
        }
    }
}

MeshObject 类扩展了 BaseObject,并增加了对象在屏幕上具有 3D 网格表示的能力。它包含一个名为 `startupColladaModelObject` 的函数,该函数接收一个 Collada XML 文档,将其加载为网格,对其进行纹理化,然后将结果添加到 Away3D 场景中。

RotatingModel.as
package
{
    import away3d.core.utils.Init;
    
    public class RotatingMesh extends MeshObject
    {
        public function RotatingMesh(engineManager:EngineManager)
        {
            super(engineManager);
        }
        
        public function startupRotatingMesh(collada:XML, init:Init):RotatingMesh
        {
            super.startupColladaModelObject(collada, init);
            return this;
        }
        
        public override function enterFrame(dt:Number):void
        {
            this.model.rotationY += 90 * dt;
        }

    }
}

RotatingModel 类是如何使用 MeshObject(因此也是 BaseObject)类的一个示例。RotatingModel 扩展了 MeshObject,然后覆盖了 `enterFrame` 函数,以便每帧围绕模型旋转一小角度。如您所见,加载模型、将其添加到场景并每帧执行更新的工作量非常少:大部分工作已经完成,这要归功于 MeshObjectBaseObject 类。

ResourceManager.as
package
{
    import away3d.materials.BitmapMaterial;
    import flash.utils.ByteArray;
    
    public class ResourceManager
    {
        [Embed (source="../media/fighter1.dae", 
                mimeType="application/octet-stream")]
        public static const Fighter1:Class;
        public var Fighter1XML:XML = null;
        
        [Embed (source="../media/sf-02.jpg")]
        public static const SF02:Class;
        public var SF02_Tex:BitmapMaterial= null;
        
        public function ResourceManager()
        {
            
        }
        
        public function loadResources():void
        {
            Fighter1XML = ConvertToXML(Fighter1);
            SF02_Tex = new BitmapMaterial(new SF02().bitmapData);
        }
        
        protected function ConvertToXML(data:Class):XML
        {
            var byteArray:ByteArray = new data() as ByteArray; 
            return new XML(byteArray.readUTFBytes(byteArray.length))
        }

    }
}

ResourceManager 用作保存应用程序使用的任何资源的区域。您作为开发者将面临的一个问题是 Flash 安全沙箱,其中本地资源无法从位于 Web 服务器上的 SWF 文件加载,而 Web 资源无法从本地 SWF 文件加载(除非进行一些特殊处理)。ResourceManager 通过 Embed 标签利用资源嵌入,该标签实质上是将开发 PC 上的文件数据嵌入到最终的 SWF 文件中。这使得分发生成的 Flash SWF 文件变得容易,因为所有数据都包含在一个文件中,并且它克服了加载资源的任何安全问题。

如您所见,我们嵌入了两个文件。`fighter1.dae` 文件是将在屏幕上显示的 Collada 网格,而 `sf-02.jpg` 文件将用于给网格添加纹理。

loadResources 函数用于加载资源。ConvertToXML 函数接收嵌入的 `fighter1.dae` 文件(以 `ByteArray` 的形式嵌入),并将其转换回 XML 对象。

ApplicationManager.as
package
{
    import away3d.core.math.Number3D;
    import away3d.core.utils.Init;
    
    public class ApplicationManager extends BaseObject
    {
        protected var mesh:MeshObject = null;
        
        public function ApplicationManager(engineManager:EngineManager)
        {
            super(engineManager);
        }
        
        public function startupApplicationManager():ApplicationManager
        {
            super.startupBaseObject();
            mesh = new RotatingMesh(engineManager).startupRotatingMesh(
                engineManager.MyResourceManager.Fighter1XML,
                new Init({material:engineManager.MyResourceManager.SF02_Tex}));
            mesh.model.moveTo(100, -100, 2000);
            
            return this;
        }
        
        public override function shutdown():void
        {
            super.shutdown();
        }
        
        public override function enterFrame(dt:Number):void
        {
            
        }
    }
}

ApplicationManager 类定义了使用我们创建的所有其他类来实际产生所需结果的代码。在这种情况下,所需结果很简单:我们只想创建 RotatingMesh 类的实例。由于我们在之前的类中付出的努力,ApplicationManager 要做的就是创建一个新的 RotatingMesh 实例,通过调用 `startupRotatingMesh` 来初始化它,并在屏幕上稍微重新定位它。

GettingStarted.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:Application
    xmlns:mx="http://www.adobe.com/2006/mxml" 
    layout="absolute" 
    xmlns:ns1="*"
    width="600"
    height="400">
    
    <ns1:EngineManager x="0" y="0" 
      width="100%" height="100%" 
      id="engineManager"/>
    
</mx:Application>

最后,我们有了 `GettingStarted.mxml` 文件。如您所见,我们将 EngineManager 添加到其中,就像它是一个普通控件,如按钮或文本框一样。因为我们实现了使 EngineManager 成为 Flex 组件所必需的函数,所以这就是所需的全部代码。

我们为这样一个简单的程序讨论了很多代码,但创建这个初始框架确实节省了很多后续时间,所以它值得额外的初步努力。

在此处查看在线演示 此处

历史

  • 2009 年 8 月 23 日 - 首次发布。
© . All rights reserved.