StarlingManual:自定义显示对象

来自Starling中文站
跳转到: 导航, 搜索

Starling的优势在于,其代码完全基于ActionScript3!而且,它用到的仅仅是公开的APIs,并没有依赖任何扩展C++代码。

因此,您可以对Starling做出原来不可能在原生显示列表中做到的扩展。本节将介绍完成这一任务的最有力办法:它将向您展示如何构建一个自定义显示对象。即,一个包含自己特有的渲染方法的、直接继承DisplayObject的子类。

注意:本教程需要Starling1.1或者从Git上得到的最新修订版本!

当您直接使用Stage3D时,请使能Starling中的错误检测功能:

starling.enableErrorChecking = true;

当您使能这项功能后,每当您的程序失控的时候,会自动跑出一个异常,这可以大大方便您的调试。当您确保您的程序没有问题后,您需要重新将错误检测功能禁止,因为此功能将对性能产生负面影响。

目录

多边形类

首先,我们会通过写一个简单的多边形类来学习如何达到我们的目的。这个类的功能为:渲染规则的、任意边数的、并且能够自定义颜色的多边形。下面是应该输出的图形:

Polygons.png

写出来的类应该可以按照下面的例子运行,就像其他显示对象一样!

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信息都在里面。

翻译:梁建锋(ljfdestiny)

个人工具
名字空间

变换
操作
导航
Starling中文资料
Starling原创教程
论坛
友链
工具箱