StarlingManual:自定义显示对象
Starling的优势在于,其代码完全基于ActionScript3!而且,它用到的仅仅是公开的APIs,并没有依赖任何扩展C++代码。
因此,您可以对Starling做出原来不可能在原生显示列表中做到的扩展。本节将介绍完成这一任务的最有力办法:它将向您展示如何构建一个自定义显示对象。即,一个包含自己特有的渲染方法的、直接继承DisplayObject的子类。
注意:本教程需要Starling1.1或者从Git上得到的最新修订版本!
当您直接使用Stage3D时,请使能Starling中的错误检测功能:
starling.enableErrorChecking = true;
当您使能这项功能后,每当您的程序失控的时候,会自动跑出一个异常,这可以大大方便您的调试。当您确保您的程序没有问题后,您需要重新将错误检测功能禁止,因为此功能将对性能产生负面影响。
目录 |
多边形类
首先,我们会通过写一个简单的多边形类来学习如何达到我们的目的。这个类的功能为:渲染规则的、任意边数的、并且能够自定义颜色的多边形。下面是应该输出的图形:
写出来的类应该可以按照下面的例子运行,就像其他显示对象一样!
var polygon:Polygon = new Polygon(50, 6, Color.RED); // radius, edges, color polygon.x = 60; polygon.y = 60; addChild(polygon);
类概述
我们先来看看这个多边形类的架构!下面是您必须实现的最少的方法:
public class Polygon extends DisplayObject { public Polygon(radius:Number, numEdges:int=6, color:uint=0xffffff); public override function dispose():void; public override function getBounds(targetSpace:DisplayObject, resultRect:Rectangle=null):Rectangle; public override function render(support:RenderSupport, alpha:Number):void; }
最上面的两个方法实现了对象的生成和销毁(谨记:在Stage3D里面,当该资源不再需要使用时必须将其清理!)
剩下的就是定义我们多边形类的功能行为:
- ggetBounds:在一个特定的坐标系统中计算多边形的边界。当您实现了这个方法,您可以自由地实现另外许许多多依赖这个方法的方法(例如:宽和高属性、hitTest方法等)。
- render:在屏幕上进行渲染。
在这些方法里面,render方法无疑是最难实现的一个环节!它包含了纯粹的、低层次的Stage3D API,这些正是Starling努力向您隐藏的部分(Starling已经将其对外封装好了,对于一般功能,直接使用就OK了~)
但是,这就是为什么我们在这里(我们要实现自定义显示对象啊!),对不对?所以尽管尝试去使用这些底层的接口吧!
在下面的章节中,我们将着眼于Polygon类需要的代码片段。在本节的最后,您将看到完整的源代码。
顶点数据
当我们在讨论Stage3D的时候,其实我们就是在讨论顶点和三角形!所有使用Stage3D渲染的对象都被建立成三角形的组合,而三角形则由其3个顶点组成!
而每一个正规的多边形,都能分解为一个或多个三角形,大家可以以右边的五边形作为参考例子。
它由六个顶点构成组成五个三角形(六个顶点,因为加上中心点)。我们给每个顶点分配一个标号(0~5),标号5代表其中心。
每一个顶点都有一个确定的位置和颜色(在例子中,每一个顶点的颜色都是相同的)。考虑到顶点在Stage3D的应用中如此重要,所以Starling里面包含一个非常有用的顶点管理类VertexData。
有了这个类,可以相当简单地创建规则多边形的顶点,下面是其代码:
// member variable: private var mVertexData:VertexData; // code: mVertexData = new VertexData(numEdges+1); mVertexData.setUniformColor(color); mVertexData.setPosition(numEdges, 0.0, 0.0); // center vertex for (var i:int=0; i<numEdges; ++i) { var edge:Point = Point.polar(radius, i * 2*Math.PI / numEdges); mVertexData.setPosition(i, edge.x, edge.y); }
上面的代码创建了一个顶点数据对象,这个对象包含了多边形边数+1个同一颜色的顶点(+1为中心点)。中心点在(0,0),而其他顶点则位于以该中心点为圆心的一个圆上。
现在,我们需要定义组成这个多边形的三角形。我们通过创建一个Vector,其中包含一个接一个的三角形,其中每个三角形的顶点由引用顶点对象的里面的3个顶点索引值构成。
在我们的多边形例子中(五边形的例子),Vector内的内容应该是这样的:
5, 0, 1, 5, 1, 2, 5, 2, 3, 5, 3, 4, 5, 4, 0
下面是其代码:
// member variable: private var mIndexData:Vector.<uint>; // code: mIndexData = new <uint>[]; for (var i:int=0; i<numEdges; ++i) mIndexData.push(numEdges, i, (i+1) % numEdges);
这就是渲染一个对象所用到的所有信息。谨记:在Stage3D里,渲染的步骤永远都是这样!无论您需要渲染的对象时什么,将其分解成由顶点组成的三角形,就是这么多!
对象的边界
现在我们的VertexData对象里面拥有所有的顶点数据,所以,我们就能够创建我们需要的多边形边界了(矩形)。这就是getBounds方法的功能。
public override function getBounds(targetSpace:DisplayObject, resultRect:Rectangle=null):Rectangle { if (resultRect == null) resultRect = new Rectangle(); var transformationMatrix:Matrix = getTransformationMatrix(targetSpace); return mVertexData.getBounds(transformationMatrix, 0, -1, resultRect); }
就像上面列出的代码,其实没多大的工作量~
第一行创建了一个用于保存结果的Rectangle对象,如果调用者已经提供一个Rectangle对象,则不再新建(减少性能消耗)。
接着,我们创建了一个matrix对象,此matrix对象描述了数学上的两个坐标系统(多边形本身的坐标系统以及传入的坐标系统)是相互关联的。就是说:这个matrix对象可以用于计算我们的顶点位于目标空间的位置。
实际上,计算边界范围的是顶点数据对象。我们需要预先把numVetices参数设置为-1,使得边界检测用上所有的顶点。
这个方法的优点在于,我们可以利用它得到一系列的新功能。例如,width、height和hitTest方法,都是默认使用这个边界方法的。
顶点和索引缓冲区
尽管,我们创建了上面那么多的数据(mVertexData 和 mIndexData)均需要上传到GPU。在Stage3D中,还需要创建VertexBuffer 和IndexBuffer对象!想像一下,那些对象只是简单的Vector/Arrays,不同在于,它们并非储存在常规的内存中(常规内存即,像其他flash对象一样工作),而是储存在图像储存器中(即显存)。
// member variables: private var mVertexBuffer:VertexBuffer3D; private var mIndexBuffer:IndexBuffer3D; // code: private function createBuffers():void { var context:Context3D = Starling.context; if (context == null) throw new MissingContextError(); if (mVertexBuffer) mVertexBuffer.dispose(); if (mIndexBuffer) mIndexBuffer.dispose(); mVertexBuffer = context.createVertexBuffer(mVertexData.numVertices, VertexData.ELEMENTS_PER_VERTEX); mVertexBuffer.uploadFromVector(mVertexData.rawData, 0, mVertexData.numVertices); mIndexBuffer = context.createIndexBuffer(mIndexData.length); mIndexBuffer.uploadFromVector(mIndexData, 0, mIndexData.length); }
现在GPU知道所有的顶点以及三角形的位置和颜色,但还不知道怎样去渲染它们。
渲染
Render方法实际上就是绘制一个对象!每个显示对象每一帧频执行一次!这个方法对性能无疑是至关重要的!
在内部其运行机制是,每个render方法都必须依赖Stage3D,这意味这flash将访问GPU(具体需要看各个不同的平台,Stage3D可能使用OpenGL或者DirextX)。
注意,您可能会有点不知所措,因为不知不觉我们已经到达GPU层级了!
在这里,我会告诉您一些基础知识,但可以这么说,在这里彻底解释Stage3D的原理是不可能的,也不是这篇文章的意旨!如果您想知道更多关于Stage3D的知识,我推荐您看一下这篇文章:How Stage3D works, Introduction to AGAL
正如我们上面所提及的,GPU需要的是任何由顶点构成的三角形。而,我们已经把这些数据上传到GPU了(通过Vertex-和Index-Buffer)!
为了指定这些三角形如何被渲染,您需要些特定的直接由GPU处理的程序:着色器。它们有两种类型:
- 顶点着色器,为每个顶点处理一次。它们的输入是顶点的所有属性(我们上面为顶点定义的信息);而输出则是最终每个顶点的颜色以及在屏幕上的坐标点。
- 片段着色器(小弟不专业,不知道是不是叫这个名字=.=)针对每个像素(片段)执行一次。它们的输入是由三角形的三个顶点的插值属性组成的;而输出就是每一个像素的颜色。
- 一个片段着色器和一个顶点着色器构成着色器程序。
在运行这些着色器前,我们先来设置它们的相关信息!
public override function render(support:RenderSupport, alpha:Number):void { // always call this method when you write custom rendering code! // it causes all previously batched quads/images to render. support.finishQuadBatch(); // (1) var alphaVector:Vector.<Number> = new <Number>[1.0, 1.0, 1.0, alpha * this.alpha]; var context:Context3D = Starling.context; // (2) if (context == null) throw new MissingContextError(); // apply the current blendmode (3) support.applyBlendMode(false); // activate program (shader) and set the required attributes / constants (4) context.setProgram(Starling.current.getProgram(PROGRAM_NAME)); context.setVertexBufferAt(0, mVertexBuffer, VertexData.POSITION_OFFSET, Context3DVertexBufferFormat.FLOAT_3); context.setVertexBufferAt(1, mVertexBuffer, VertexData.COLOR_OFFSET, Context3DVertexBufferFormat.FLOAT_4); context.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, support.mvpMatrix, true); context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 4, sRenderAlpha, 1); // finally: draw the object! (5) context.drawTriangles(mIndexBuffer, 0, mNumEdges); // reset buffers (6) context.setVertexBufferAt(0, null); context.setVertexBufferAt(1, null); }
注意下面部分需要对照上面的程序才容易看明白!
(1) 因为所有在Starling中的显示对象都是规则的四边形,它包含了一种性能优化的方法——将尽量多的四边形一起渲染(造成一个批处理,一次调用,渲染尽可能多的四边形)。我们的多边形并非一个四边形,而且我们需要告诉Starling在我们接管之前它必须完成之前的批次。在自定义渲染方法中您必须这么做。
(2) 然后我们需要从Starling中获得Context3D对象。这是Stage3D的核心:所有渲染都是通过context完成的!
(3) 显示对象支持不同的混合模式。这个方法激活需要的一个。
(4) 我们激活程序执行实际的绘图。这个程序需要输入参数。输入提供两种方法:
- 通过一个Vertex Buffer:这个缓存器我们已经在上面设定好了,里面包含所有的顶点数据。GPU会将该数据一个接一个放进该程序。
- 存储在索引0的是位置向量(大小为3个float)
- 存储在索引1的是颜色向量(大小为4个float)
- 通过程序常量,这些常量对于每个程序处理都是相同的。
- 顶点着色器接收转换矩阵(索引0)
- 还有透明度向量(索引0)
(5) 就是这么多了,现在我们就可以绘制对象了。
(6) 最后我们需要做得是重置存储器~ 使得Stage3D重新变得干净、舒适。
AGAL
现在这个部分就相对有点棘手了:我们需要着手写着色器程序!
其实它本身并没有那么复杂。您在游戏开发中做的可能比这更复杂!让您觉得它复杂的一点可能是,它并不是使用ActionScript,而是使用AGAL——Adobe开发的一种汇编语言!
我们先来看看代码:
private static var PROGRAM_NAME:String = "polygon"; private static function registerPrograms():void { var target:Starling = Starling.current; if (target.hasProgram(PROGRAM_NAME)) return; // already registered var vertexProgramCode:String = "m44 op, va0, vc0 \n" + // 4x4 matrix transform to output space "mul v0, va1, vc4 \n"; // multiply color with alpha and pass it to fragment shader var fragmentProgramCode:String = "mov oc, v0"; // just forward incoming color var vertexProgramAssembler:AGALMiniAssembler = new AGALMiniAssembler(); vertexProgramAssembler.assemble(Context3DProgramType.VERTEX, vertexProgramCode); var fragmentProgramAssembler:AGALMiniAssembler = new AGALMiniAssembler(); fragmentProgramAssembler.assemble(Context3DProgramType.FRAGMENT, fragmentProgramCode); target.registerProgram(PROGRAM_NAME, vertexProgramAssembler.agalcode, fragmentProgramAssembler.agalcode); }
首先我们定义了一个属于我们程序的名字,使得在渲染的过程中我们能够再次访问它。然后,就开始AGAL编程了~!
在AGAL里面,每一行都包含一个简单的方法调用。
[opcode] [destination], [argument1], ([argument2])
- 前三个字母是操作数的名称(m44, mov)。
- 第一个参数定义了方法的运行结果存储到哪里。
- 其他的参数实际上就是该方法的参数。
- 所有数据都保存在预定义的寄存器,就像变量。
其中一些寄存器中已经包含了数据,就是我们的渲染设定(上一节写到的)。
va0, va1, ... -> Vertex Attributes, set up with 'setVertexBufferAt' vc0, vc1, ... -> Vertex Constants, set up with 'setProgramConstants' fc0, fc1, ... -> Fragment Constants, set up with 'setProgramConstants'
这是其他类型的寄存器。例如,输出或者临时数据寄存器。您可以通过任何一个AGAL文档学习他们。
下面是我们的顶点着色器代码:
m44 op, va0, vc0 // -> read: op = va0 * vc0 mul v0, va1, vc4 // -> read: v0 = va1 * vc4
第一行是,顶点位置与一个转换矩阵相乘。其中矩阵是由Starling提供的。而其结果是“剪辑空间”中顶点的位置(例如屏幕上的坐标)。
m44: 4×4 matrix multiplication op: output point va0: vertex attribute 0 (contains the vertex position) vc0: vertex constant 0 (contains the transformation matrix)
第二行则是,顶点透明度与顶点颜色相乘的值。结果则保存在v0中(这是一个会被注入片段着色器的寄存器varying register 0)。
然后就是将v0移动到片段着色器中:
mov oc, v0 // -> read: oc = v0
在这里,其实我们并不需要做太多的东西!因为顶点着色器已经保存了所有的颜色信息在v0寄存器了。我们只需要将v0寄存器中的值复制到输出寄存器中。
- mov:移动(复制)操作数
- oc:输出颜色
- v0:顶点常量0(我们需要将其在顶点着色器中准备它们)
如果我们需要在多边形上面显示一个纹理,我们需要在这里访问纹理,然后将纹理的颜色(和透明度)与多边形的顶点的颜色(和透明度)相乘。
其余的函数则是编译代码和注册程序到正在使用的Starling实例。
好的,我们搞掂了~现在我们能绘制多边形了!
处理丢失的情况
对了!还有一件事需要去做。在部分平台上面(Andoroid,Windows),Starling的渲染context可能会在一些特定的情况下丢失!在Android平台下,这通常会发生在设备旋转的时候;而在Windows平台上,锁定屏幕会触发设备丢失。
为了使Starling处理设备丢失的情况,用户可以在生成Starling实例前调用下面的方法。
Starling.handleLostContext = true;
我们自定义的类能够处理这种情况了~ 还真得谢谢Starling,使这一切变得容易~只需要简单的在您得构造函数中加入下面的事件处理:
Starling.current.addEventListener(Event.CONTEXT3D_CREATE, onContextCreated);
在对应的事件侦听器,我们重新创建顶点缓冲器和重新注册我们的程序,在完整的程序中,这两个部分已经被写成方法,所以我们只需要简单地调用它们即可。
private function onContextCreated(event:Event):void { createBuffers(); registerPrograms(); }
不要忘记去除侦听器哦~
结论
恭喜您,我们已经成功创建了一个自定义显示对象。您可以使用该类作为框架继续写属于您自己的对象。
该类完整的源代码可以在GitHub上找到!里面还包含了一些小的优化(这些部分我在上面的阐述中跳过了),例如:它使用几个静态辅助变量,以避免在渲染过程中创建的临时对象,保持最低限度的调用GC。
在此能找打完整的类: GitHub: Polygon.as
Where to go from here
这仅仅是一个开始,当然,这里还有一些可以升级的空间。
- 为每个顶点设置不同的颜色。这将产生很酷的颜色渐变效果!
- 在多边形上面增加纹理。
- 重写hitTest方法,使其只在碰撞到真实的三角形时才会返回碰撞信息(上面写得hitTest方法只是单纯的矩形碰撞检测,并不精确)。
如果您成功完成上面的任务,不要忘记分享您的成功经验哦~(只需要简单的上传您得代码就可以了,谢谢!)
Good luck!
顺便说一句,这里有非常好的 Stage3D Shader Cheat Sheet 总结,所有的AGAL信息都在里面。