Away3D 入门





5.00/5 (1投票)
了解如何使用 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
的集合,这就是 baseObjects
、newBaseObjects
和 removedBaseObjects
属性的由来。baseObjects
属性保存所有当前活动的 BaseObject
,而 newBaseObjects
和 removedBaseObjects
包含刚刚添加到系统或从中移除的 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
类中大部分代码的含义。addBaseObject
和 removeBaseObject
函数分别将 BaseObject
添加到 newBaseObjects
和 removedBaseObjects
集合中,而 insertNewBaseObjects
和 removeDeletedBaseObjects
函数(从 onEnterFrame
函数调用)用于将主 baseObjects
集合与已添加和已移除的 BaseObject
同步。
EngineManager
包含六个在 Flash/Flex 组件生命周期中使用的函数。其中四个在 Adobe 文档中有详细介绍,我在这里将一一介绍。
measure
函数用于定义控件的最小和默认大小。这非常直接,因为我们只需要为底层 UIComponent
的 measuredMinWidth
、measuredMinHeight
、measuredHeight
和 measuredWidth
属性赋值即可。
updateDisplayList
函数用于调整控件子项的大小和位置。在这种情况下,我们唯一的子元素是 Away3D 引擎,具体来说是 View3D 对象。在这里,我们只是调整 `view` 属性的大小,以反映 EngineManager
控件大小的变化。
commitProperties
函数用于允许控件应用任何已做的属性更改。其理念是,属性可以而且会按任何顺序更改,但可能需要按特定顺序应用或处理。尽管 EngineManager
没有公开任何可更改的属性,但这里的代码设置用于重新初始化 ApplicationManager
(稍后将详细介绍该类),这实际上会重启应用程序。
createChildren
函数在控件预期创建任何子项时被调用。如前所述,EngineManager
控件的唯一子项是 Away3D 引擎;但是,我们此时并不创建引擎。Away3D 引擎会多次引用 `stage` 属性,而该属性在 `ADDED_TO_STAGE` 事件触发之前为 `null`。因此,我们将 `createChildrenEx` 函数附加到此事件,然后在事件触发时创建 Away3D 引擎。同样,我们使用 `REMOVED_FROM_STAGE` 来调用 `shutdown`,它将清理 Away3D 引擎。
updateDisplayList
、commitProperties
、createChildren
和 measure
函数都具有由 UIComponent
类定义的函数。还有另外两个函数,shutdown
和 createChildrenEx
,它们也起着重要作用。
如上所述,createChildrenEx
函数是 Away3D 引擎实际启动的地方。对于这个简单的示例,我们只需要两个 Away3D 类:Camera3D
和 View3D
。Camera3D
类(我们将其分配给 `cam` 属性)是我们观察 Away3D 世界的相机。View3D
类(我们将其分配给 `view` 属性)负责将 3D 世界渲染到您的 2D 显示器上。初始化 Away3D 引擎的实际代码不超过几行。我们创建一个 Camera3D
类的新实例,然后创建一个 View3D
类的新实例。然后我们定义我们想要的渲染器类型(在本例中是基本的渲染器),并将 View3D
添加为 EngineManager
控件的子元素。
除了初始化 Away3D 引擎外,createChildrenEx
函数还负责创建新的 ResourceManager
和 ApplicationManager
,并将 `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` 时更新自身。startupBaseObject
和 shutdown
函数用于将 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` 函数,以便每帧围绕模型旋转一小角度。如您所见,加载模型、将其添加到场景并每帧执行更新的工作量非常少:大部分工作已经完成,这要归功于 MeshObject
和 BaseObject
类。
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 日 - 首次发布。